timber 1.0.3 → 1.0.4

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 8580577f8c85abeb5eacec510136a2bf05d72406
4
- data.tar.gz: 7a08aa6f00eda500d016c413ccf2b95d759b9266
3
+ metadata.gz: 08cb61746c20295dafbfcf1a098664dd9a7ac0ce
4
+ data.tar.gz: f7a59dd084e99faac34169d37bc49f33a0aafba0
5
5
  SHA512:
6
- metadata.gz: b906867c8198c1d94052856b6e7b402af88bd252b770ed9a8a33aae7284a3f562569f4fde7202c759eb786e28f8b85b0b5a2b1ab8c2b5d63bde56f1ee686d404
7
- data.tar.gz: d4afc694df19c1c90a4711b7d1762bb0f203cf4521aa44a1f63d5e7f66c8cef37258f7af2c8e6fb73b166305aa43a1b4bfce3a2ea0c625e1951e9bc0726657dc
6
+ metadata.gz: 40ffc77bceab9aec868f3f0592054970bd535abcdc6fda63101859605cd68a392faf2a7f7f13970cd6091370f2b711dc2f52072cede1f2d3aee760ef76274ba6
7
+ data.tar.gz: c2b7781da71ee771da0bb771295b83166f22446cf77bc27905d1a08311079b4b1a7ac3f43ca0011002148dff372f185c3180845905e8b8646960624278122759
data/README.md CHANGED
@@ -11,34 +11,48 @@
11
11
 
12
12
 
13
13
  1. [What is timber?](#what-is-timber)
14
- 1. [How does it work?](#what-is-timber)
15
- 2. [Logging Custom Events](#logging-custom-events)
16
- 3. [The Timber Console / Pricing](#the-timber-console-pricing)
17
- 2. [Install](#install)
14
+ 2. [Why timber?](#why-timber)
15
+ 3. [How does it work?](#how-does-it-work)
16
+ 4. [Logging Custom Events](#logging-custom-events)
17
+ 5. [The Timber Console / Pricing](#the-timber-console-pricing)
18
+ 6. [Install](#install)
18
19
 
19
20
 
20
21
  ## What is Timber?
21
22
 
22
- Timber automatically structures your logs with events and context in a non-proprietary JSON format.
23
- It’s simple, quick, managed, and has absolutely no risk of code debt or lock-in.
24
- It’s just good ol’ logging.
23
+ Glad you asked! :) Timber takes a different approach to logging, in that it automatically
24
+ enriches and structures your logs without altering the essence of your original log messages.
25
+ Giving you the best of both worlds: human readable logs *and* rich structured data.
25
26
 
26
- Timber’s philosophy is that application insight should be open and owned by you.
27
- And there is no better vehicle than logging:
27
+ And it does so with absolutely no lock-in or risk of code debt. It's just good ol' loggin'™!
28
+ For example:
28
29
 
29
- 1. It’s a shared practice that has been around since the dawn of computers.
30
- 2. It’s baked into every language, library, and framework. Even your own apps.
31
- 3. The data is entirely owned by you.
30
+ 1. The resulting log format, by deafult, is a simple, non-proprietary, JSON structure.
31
+ 2. The [`Timber::Logger`](lib/timber/events) class extends `Logger`, and will never change or
32
+ extend the public API.
33
+ 3. Where you send your logs is entirely up to you, but we hope you'll check out
34
+ [timber.io](https://timber.io). We've built a beautiful, modern, and *fast* console specifically
35
+ for the strutured data captured here.
32
36
 
33
- The problem is that logs are messy, noisy, and hard to use. Timber solves this by being
34
- application aware, properly structuring your logs, and optionally providing a [fast, modern,
35
- and beautiful console](https://timber.io) -- allowing you to realize the power of
36
- your logs.
37
+
38
+ ## Why Timber?
39
+
40
+ Timber’s philosophy is that application insight should be open and owned by you. It should not
41
+ require a myriad of services to accomplish. And there is no better vehicle than logging:
42
+
43
+ 1. The log is immutable and complete. [It is the truth](http://files.timber.io/images/log-is-the-truth.png) :)
44
+ 2. It’s a shared practice that has been around since the dawn of computers.
45
+ 3. It’s baked into every language, library, and framework. Even your own apps!
46
+ 4. The data is open, accessible, and entirely owned by you. Yay!
47
+
48
+ The problem is that logs are unstructured, noisy, and hard to use. `grep` can only take you so
49
+ far! Timber solves this by properly structuring your logs, making them easy to search and
50
+ visualize -- enabling you to sanely realize the power of your logs.
37
51
 
38
52
 
39
53
  ## How does it work?
40
54
 
41
- Glad you asked! :) Timber automatically structures your logs by taking advantage of public APIs.
55
+ Timber automatically structures your logs by taking advantage of public APIs.
42
56
 
43
57
  For example, by subscribing to `ActiveSupport::Notifications`, Timber can automatically turn this:
44
58
 
@@ -76,12 +90,12 @@ Into this:
76
90
  ```
77
91
 
78
92
  It does the same for `http requests`, `sql queries`, `exceptions`, `template renderings`,
79
- and any other event your framework logs. (for a full list see `Timber::Events`)
93
+ and any other event your framework logs. (for a full list see [`Timber::Events`](lib/timber/events))
80
94
 
81
95
 
82
96
  ## Logging Custom Events
83
97
 
84
- > Another service? More code debt? :*(
98
+ > Another service? More lock-in? :*(
85
99
 
86
100
  Nope! Logging custom events is Just Logging™. Check it out:
87
101
 
@@ -101,41 +115,9 @@ end
101
115
  Logger.warn PaymentRejectedEvent.new("abcd1234", 100, "Card expired")
102
116
  ```
103
117
 
104
- (for more examples, see the `Timber::Logger` docs)
105
-
106
- No mention of Timber anywhere! In fact, this approach pushes things the opposite way. What if,
107
- as a result of structured logging, you could start decoupling other services from your application?
108
-
109
- Before:
110
-
111
- ```
112
- |---[HTTP]---> sentry / bugsnag / etc
113
- My Application |---[HTTP]---> librato / graphite / etc
114
- |---[HTTP]---> new relic / etc
115
- |--[STDOUT]--> logs
116
- |---> Logging service
117
- |---> S3
118
- |---> RedShift
119
- ```
120
-
121
-
122
- After:
123
-
124
- ```
125
- |-- sentry / bugsnag / etc
126
- |-- librato / graphite / etc
127
- My Application |--[STDOUT]--> logs ---> Timber ---> |-- new relic / etc
128
- ^ |-- S3
129
- | |-- RedShift
130
- | ^
131
- fast, efficient, durable, |
132
- replayable, auditable, change any of these without
133
- just logging touching your code
134
- *and* backfill them!
135
- ```
136
-
137
- [Mind-blown!](http://i.giphy.com/EldfH1VJdbrwY.gif)
118
+ (for more examples, see [the `Timber::Logger` docs](lib/timber/logger.rb))
138
119
 
120
+ No mention of Timber anywhere!
139
121
 
140
122
 
141
123
  ## The Timber Console / Pricing
@@ -186,6 +168,19 @@ after you add your application.
186
168
 
187
169
  *Other transport methods coming soon!*
188
170
 
171
+
172
+ #### Rails TaggedLogging?
173
+
174
+ No probs! Use it as normal, Timber will even pull out the tags and include them in the `context`.
175
+
176
+ ```ruby
177
+ config.logger = ActiveSupport::TaggedLogging.new(Timber::Logger.new(STDOUT))
178
+ ```
179
+
180
+ **Warning**: Tags lack meaningful descriptions, they are a poor mans context. Not to worry though!
181
+ Timber provides a simple system for adding custom context that you can optionally use. Checkout
182
+ [the `Timber::CurrentContext` docs](lib/timber/current_context.rb) for examples.
183
+
189
184
  ---
190
185
 
191
186
  That's it! Log to your heart's content.
@@ -1,5 +1,6 @@
1
1
  # core classes
2
2
  require "json" # brings to_json to the core classes
3
+ require "msgpack" # brings to_msgpack to the core classes
3
4
 
4
5
  # Base (must come first, order matters)
5
6
  require "timber/config"
@@ -2,8 +2,14 @@ module Timber
2
2
  # Base class for all `Timber::Contexts::*` classes.
3
3
  # @private
4
4
  class Context
5
+ class << self
6
+ def keyspace
7
+ @keyspace || raise(NotImplementedError.new)
8
+ end
9
+ end
10
+
5
11
  def keyspace
6
- raise NoImplementedError.new
12
+ self.class.keyspace
7
13
  end
8
14
 
9
15
  def as_json(options = {})
@@ -1,6 +1,7 @@
1
1
  require "timber/contexts/custom"
2
2
  require "timber/contexts/http"
3
3
  require "timber/contexts/organization"
4
+ require "timber/contexts/tags"
4
5
  require "timber/contexts/user"
5
6
 
6
7
  module Timber
@@ -8,6 +8,8 @@ module Timber
8
8
  # # ... anything logged here will have the context ...
9
9
  # end
10
10
  class Custom < Context
11
+ @keyspace = :custom
12
+
11
13
  attr_reader :type, :data
12
14
 
13
15
  def initialize(attributes)
@@ -15,10 +17,6 @@ module Timber
15
17
  @data = attributes[:data] || raise(ArgumentError.new(":data is required"))
16
18
  end
17
19
 
18
- def keyspace
19
- :custom
20
- end
21
-
22
20
  def as_json(_options = {})
23
21
  {type => data}
24
22
  end
@@ -7,6 +7,8 @@ module Timber
7
7
  # @note This context should be installed automatically through probes,
8
8
  # such as the {Probes::RackHTTPContext} probe.
9
9
  class HTTP < Context
10
+ @keyspace = :http
11
+
10
12
  attr_reader :method, :path, :remote_addr, :request_id
11
13
 
12
14
  def initialize(attributes)
@@ -16,10 +18,6 @@ module Timber
16
18
  @request_id = attributes[:request_id]
17
19
  end
18
20
 
19
- def keyspace
20
- :http
21
- end
22
-
23
21
  def as_json(_options = {})
24
22
  {:method => method, :path => path, :remote_addr => remote_addr, :request_id => request_id}
25
23
  end
@@ -16,6 +16,8 @@ module Timber
16
16
  # end
17
17
  #
18
18
  class Organization < Context
19
+ @keyspace = :organization
20
+
19
21
  attr_reader :id, :name
20
22
 
21
23
  def initialize(attributes)
@@ -23,10 +25,6 @@ module Timber
23
25
  @name = attributes[:name]
24
26
  end
25
27
 
26
- def keyspace
27
- :organization
28
- end
29
-
30
28
  def as_json(_options = {})
31
29
  {id: id, name: name}
32
30
  end
@@ -0,0 +1,22 @@
1
+ module Timber
2
+ module Contexts
3
+ # Adds unnamed tags to the context.
4
+ #
5
+ # **Warning:** It is highly recommend that you use custom contexts instead. As they are
6
+ # more descriptive. This module exists primarily to support the ActiveSupport::TaggedLogging
7
+ # antipattern.
8
+ class Tags < Context
9
+ @keyspace = :tags
10
+
11
+ attr_reader :values
12
+
13
+ def initialize(attributes)
14
+ @values = attributes[:values] || raise(ArgumentError.new(":values is required"))
15
+ end
16
+
17
+ def as_json(_options = {})
18
+ values
19
+ end
20
+ end
21
+ end
22
+ end
@@ -17,6 +17,8 @@ module Timber
17
17
  # end
18
18
  #
19
19
  class User < Context
20
+ @keyspace = :user
21
+
20
22
  attr_reader :id, :name
21
23
 
22
24
  def initialize(attributes)
@@ -24,10 +26,6 @@ module Timber
24
26
  @name = attributes[:name]
25
27
  end
26
28
 
27
- def keyspace
28
- :user
29
- end
30
-
31
29
  def as_json(_options = {})
32
30
  {id: id, name: name}
33
31
  end
@@ -4,40 +4,101 @@ module Timber
4
4
  # Holds the current context in a thread safe memory storage. This context is
5
5
  # appended to every log line. Think of context as join data between your log lines,
6
6
  # allowing you to relate them and filter them appropriately.
7
+ #
8
+ # @note Because context is appended to every log line, it is recommended that you limit this
9
+ # to only neccessary data needed to relate your log lines.
7
10
  class CurrentContext
8
11
  include Singleton
9
12
 
10
13
  THREAD_NAMESPACE = :_timber_current_context.freeze
11
14
 
12
15
  class << self
13
- # Convenience method for {#with}.
14
- #
15
- # @example Adding a context
16
- # custom_context = Timber::Contexts::Custom.new(type: :keyspace, data: %{my: "data"})
17
- # Timber::CurrentContext.with(custom_context) do
18
- # # ... anything logged here will have the context ...
19
- # end
16
+ # Convenience method for {#with}. See {#with} for full details and examples.
20
17
  def with(*args, &block)
21
18
  instance.with(*args, &block)
22
19
  end
20
+
21
+ # Convenience method for {#add}. See {#add} for full details and examples.
22
+ def add(*args)
23
+ instance.add(*args)
24
+ end
25
+
26
+ # Convenience method for {#remove}. See {#remove} for full details and examples.
27
+ def remove(*args)
28
+ instance.remove(*args)
29
+ end
30
+
31
+ def hash(*args)
32
+ instance.hash(*args)
33
+ end
23
34
  end
24
35
 
25
- # Adds a context to the current stack.
26
- def with(data)
27
- key = data.keyspace
28
- hash[key] = data
36
+ # Adds a context and then removes it when the block is finished executing.
37
+ #
38
+ # @note Because context is included with every log line, it is recommended that you limit this
39
+ # to only neccessary data.
40
+ #
41
+ # @example Adding a custom context
42
+ # custom_context = Timber::Contexts::Custom.new(type: :organization, data: %{id: 1, name: "Timber"})
43
+ # Timber::CurrentContext.with(custom_context) do
44
+ # # ... anything logged here will include the context ...
45
+ # end
46
+ # # Be sure to checkout Timber::Contexts! These are officially supported and many of these
47
+ # # will be automatically included via Timber::Probes
48
+ #
49
+ # @example Adding multiple contexts
50
+ # Timber::CurrentContext.with(context1, context2) { ... }
51
+ def with(*contexts)
52
+ add(*contexts)
29
53
  yield
30
54
  ensure
31
- hash.delete(key)
55
+ contexts.each do |context|
56
+ if context.keyspace == :custom
57
+ # Custom contexts are merged and should be removed the same
58
+ hash[context.keyspace].delete(context.type)
59
+ else
60
+ remove(context)
61
+ end
62
+ end
32
63
  end
33
64
 
34
- def snapshot
35
- hash.clone
65
+ # Adds contexts but does not remove them. See {#with} for automatic maintenance and {#remove}
66
+ # to remove them yourself.
67
+ #
68
+ # @note Because context is included with every log line, it is recommended that you limit this
69
+ # to only neccessary data.
70
+ def add(*contexts)
71
+ contexts.each do |context|
72
+ key = context.keyspace
73
+ json = context.as_json # Convert to json now so that we aren't doing it for every line
74
+ if key == :custom
75
+ # Custom contexts are merged into the space
76
+ hash[key] ||= {}
77
+ hash[key].merge(json)
78
+ else
79
+ hash[key] = json
80
+ end
81
+ end
36
82
  end
37
83
 
38
- private
39
- def hash
40
- Thread.current[THREAD_NAMESPACE] ||= {}
84
+ # Removes a context. This must be a {Timber::Context} type. See {Timber::Contexts} for a list.
85
+ # If you wish to remove by key, or some other way, use {#hash} and modify the hash accordingly.
86
+ def remove(*contexts)
87
+ contexts.each do |context|
88
+ hash.delete(context.keyspace)
41
89
  end
90
+ end
91
+
92
+ # The internal hash that is maintained. It is recommended that you use {#with} and {#add}
93
+ # for hash maintenance.
94
+ def hash
95
+ Thread.current[THREAD_NAMESPACE] ||= {}
96
+ end
97
+
98
+ # Snapshots the current context so that you get a moment in time representation of the context,
99
+ # since the context can change as execution proceeds.
100
+ def snapshot
101
+ hash.clone
102
+ end
42
103
  end
43
104
  end
@@ -1,19 +1,17 @@
1
- require "monitor"
2
- require "msgpack"
1
+ require "timber/log_devices/http/triggered_buffer"
3
2
 
4
3
  module Timber
5
4
  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.
5
+ # A log device that buffers and delivers log messages over HTTPS to the Timber API in batches.
6
+ # The buffer and delivery strategy are very efficient and the log messages will be delivered in
7
+ # msgpack format.
8
+ #
9
+ # See {#initialize} for options and more details.
9
10
  class HTTP
10
- class DeliveryError < StandardError; end
11
-
12
11
  API_URI = URI.parse("https://api.timber.io/http_frames")
13
- CONTENT_TYPE = "application/json".freeze
12
+ CONTENT_TYPE = "application/x-timber-msgpack-frame-1".freeze
14
13
  CONNECTION_HEADER = "keep-alive".freeze
15
14
  USER_AGENT = "Timber Ruby Gem/#{Timber::VERSION}".freeze
16
-
17
15
  HTTPS = Net::HTTP.new(API_URI.host, API_URI.port).tap do |https|
18
16
  https.use_ssl = true
19
17
  https.read_timeout = 30
@@ -23,60 +21,100 @@ module Timber
23
21
  end
24
22
  https.open_timeout = 10
25
23
  end
24
+ DELIVERY_FREQUENCY_SECONDS = 2.freeze
25
+ RETRY_LIMIT = 3.freeze
26
+ BACKOFF_RATE_SECONDS = 3.freeze
26
27
 
27
- DEFAULT_DELIVERY_FREQUENCY = 2.freeze
28
28
 
29
- # Instantiates a new HTTP log device.
29
+ # Instantiates a new HTTP log device that can be passed to {Timber::Logger#initialize}.
30
30
  #
31
31
  # @param api_key [String] The API key provided to you after you add your application to
32
32
  # [Timber](https://timber.io).
33
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
34
+ # @option attributes [Symbol] :payload_limit_bytes Determines the maximum size in bytes that
35
+ # and HTTP payload can be. Please see {TriggereBuffer#initialize} for the default.
36
+ # @option attributes [Symbol] :buffer_limit_bytes Determines the maximum size of the total
37
+ # buffer. This should be many times larger than the `:payload_limit_bytes`.
38
+ # Please see {TriggereBuffer#initialize} for the default.
39
+ # @option attributes [Symbol] :buffer_overflow_handler (nil) When a single message exceeds
40
+ # `:payload_limit_bytes` or the entire buffer exceeds `:buffer_limit_bytes`, the Proc
41
+ # passed to this option will be called with the msg that would overflow the buffer. See
42
+ # the examples on how to use this properly.
43
+ # @option attributes [Symbol] :delivery_frequency_seconds (2) How often the client should
35
44
  # attempt to deliver logs to the Timber API. The HTTP client buffers logs between calls.
45
+ #
46
+ # @example Basic usage
47
+ # Timber::Logger.new(Timber::LogDevices::HTTP.new("my_timber_api_key"))
48
+ #
49
+ # @example Handling buffer overflows
50
+ # # Persist overflowed lines to a file
51
+ # # Note: You could write these to any permanent storage.
52
+ # overflow_log_path = "/path/to/my/overflow_log.log"
53
+ # overflow_handler = Proc.new { |log_line_msg| File.write(overflow_log_path, log_line_ms) }
54
+ # http_log_device = Timber::LogDevices::HTTP.new("my_timber_api_key",
55
+ # buffer_overflow_handler: overflow_handler)
56
+ # Timber::Logger.new(http_log_device)
36
57
  def initialize(api_key, options = {})
37
58
  @api_key = api_key
38
- @buffer = []
39
- @monitor = Monitor.new
40
- @delivery_thread = Thread.new do
41
- at_exit { deliver }
59
+ @buffer = TriggeredBuffer.new(
60
+ payload_limit_bytes: options[:payload_limit_bytes],
61
+ limit_bytes: options[:buffer_limit_bytes],
62
+ overflow_handler: options[:buffer_overflow_handler]
63
+ )
64
+ @delivery_interval_thread = Thread.new do
42
65
  loop do
43
- sleep options[:frequency_seconds] || DEFAULT_DELIVERY_FREQUENCY
44
- deliver
66
+ sleep(options[:delivery_frequency_seconds] || DELIVERY_FREQUENCY_SECONDS)
67
+ buffer_for_delivery = @buffer.reserve
68
+ if buffer_for_delivery
69
+ deliver(buffer_for_delivery)
70
+ end
45
71
  end
46
72
  end
47
73
  end
48
74
 
75
+ # Write a new log line message to the buffer, and deliver if the msg exceeds the
76
+ # payload limit.
49
77
  def write(msg)
50
- @monitor.synchronize {
51
- @buffer << msg
52
- }
78
+ buffer_for_delivery = @buffer.write(msg)
79
+ if buffer_for_delivery
80
+ deliver(buffer_for_delivery)
81
+ end
82
+ true
53
83
  end
54
84
 
85
+ # Closes the log device, cleans up, and attempts one last delivery.
55
86
  def close
56
- @delivery_thread.kill
87
+ @delivery_interval_thread.kill
88
+ buffer_for_delivery = @buffer.reserve
89
+ if buffer_for_delivery
90
+ deliver(buffer_for_delivery)
91
+ end
57
92
  end
58
93
 
59
94
  private
60
- def deliver
61
- body = @buffer.read
95
+ def deliver(body)
96
+ Thread.new do
97
+ RETRY_LIMIT.times do |try_index|
98
+ request = Net::HTTP::Post.new(API_URI.request_uri).tap do |req|
99
+ req['Authorization'] = authorization_payload
100
+ req['Connection'] = CONNECTION_HEADER
101
+ req['Content-Type'] = CONTENT_TYPE
102
+ req['User-Agent'] = USER_AGENT
103
+ req.body = body
104
+ end
62
105
 
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}")
106
+ res = HTTPS.request(request)
107
+ code = res.code.to_i
108
+ if code < 200 || code >= 300
109
+ Config.instance.logger.debug("Timber HTTP delivery failed - #{res.code}: #{res.body}")
110
+ sleep((try_index + 1) * BACKOFF_RATE_SECONDS)
111
+ else
112
+ @buffer.remove(body)
113
+ Config.instance.logger.debug("Timber HTTP delivery successful - #{code}")
114
+ break # exit the loop
115
+ end
75
116
  end
76
- Config.instance.logger.debug("Success! #{code}: #{res.body}")
77
117
  end
78
-
79
- @buffer.clear
80
118
  end
81
119
 
82
120
  def authorization_payload
@@ -0,0 +1,79 @@
1
+ require "monitor"
2
+
3
+ module Timber
4
+ module LogDevices
5
+ class HTTP
6
+ # Maintains a triggered buffer, where the trigger is {PAYLOAD_LIMIT_BYTES}. Once the buffer
7
+ # exceeds this limit it will lock and return that buffer up to that point while still making
8
+ # a new buffer available for writes. This ensures that the HTTP client can attempt to deliver
9
+ # the buffer contents without blocking execution of the application.
10
+ #
11
+ # If the overall buffer exceeeds the overall limit (specified by the `:limit_bytes` option),
12
+ # then a buffer overflow is triggered. This can be customized using the `:overflow_handler`
13
+ # option.
14
+ class TriggeredBuffer
15
+ DEFAULT_PAYLOAD_LIMIT_BYTES = 5_000_000 # 5mb, the Timber API will not accept messages larger than this
16
+ DEFAULT_LIMIT_BYTES = 50_000_000 # 50mb
17
+
18
+ def initialize(options = {})
19
+ @buffers = []
20
+ @monitor = Monitor.new
21
+ @payload_limit_bytes = options[:payload_limit_bytes] || DEFAULT_PAYLOAD_LIMIT_BYTES
22
+ @limit_bytes = options[:limit_bytes] || DEFAULT_LIMIT_BYTES
23
+ @overflow_handler = options[:overflow_handler]
24
+ end
25
+
26
+ def write(msg)
27
+ if msg.bytesize > @payload_limit_bytes || (msg.bytesize + total_bytesize) > @limit_bytes
28
+ handle_overflow(msg)
29
+ return nil
30
+ end
31
+
32
+ @monitor.synchronize do
33
+ buffer = writable_buffer
34
+ if @buffers == [] || buffer.nil? || buffer.frozen?
35
+ @buffers << msg
36
+ nil
37
+ elsif (buffer.bytesize + msg.bytesize) > @payload_limit_bytes
38
+ @buffers << msg
39
+ buffer.freeze
40
+ else
41
+ buffer << msg
42
+ nil
43
+ end
44
+ end
45
+ end
46
+
47
+ def reserve
48
+ @monitor.synchronize do
49
+ buffer = writable_buffer
50
+ if buffer
51
+ buffer.freeze
52
+ end
53
+ end
54
+ end
55
+
56
+ def remove(buffer)
57
+ @monitor.synchronize do
58
+ @buffers.delete(buffer)
59
+ end
60
+ end
61
+
62
+ private
63
+ def total_bytesize
64
+ @buffers.reduce(0) { |acc, buffer| acc + buffer.bytesize }
65
+ end
66
+
67
+ def writable_buffer
68
+ @buffers.find { |buffer| !buffer.frozen? }
69
+ end
70
+
71
+ def handle_overflow(msg)
72
+ if @overflow_handler
73
+ @overflow_handler.call(msg)
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
@@ -81,6 +81,27 @@ module Timber
81
81
  end
82
82
  end
83
83
 
84
+ # Structures your log messages into Timber's hybrid format, which makes
85
+ # it easy to read while also appending the appropriate metadata.
86
+ #
87
+ # logger = Timber::Logger.new(STDOUT)
88
+ # logger.formatter = Timber::JSONFormatter.new
89
+ #
90
+ # Example message:
91
+ #
92
+ # My log message @timber.io {"level":"info","dt":"2016-09-01T07:00:00.000000-05:00"}
93
+ #
94
+ class HybridFormatter < Formatter
95
+ METADATA_CALLOUT = "@timber.io".freeze
96
+
97
+ def call(severity, time, progname, msg)
98
+ log_entry = build_log_entry(severity, time, progname, msg)
99
+ metadata = log_entry.to_json(:except => [:message])
100
+ # use << for concatenation for performance reasons
101
+ log_entry.message.gsub("\n", "\\n") << " " << METADATA_CALLOUT << " " << metadata << "\n"
102
+ end
103
+ end
104
+
84
105
  # Structures your log messages into JSON.
85
106
  #
86
107
  # logger = Timber::Logger.new(STDOUT)
@@ -97,24 +118,19 @@ module Timber
97
118
  end
98
119
  end
99
120
 
100
- # Structures your log messages into Timber's hybrid format, which makes
101
- # it easy to read while also appending the appropriate metadata.
121
+ # Structures your log messages into JSON.
102
122
  #
103
123
  # logger = Timber::Logger.new(STDOUT)
104
124
  # logger.formatter = Timber::JSONFormatter.new
105
125
  #
106
126
  # Example message:
107
127
  #
108
- # My log message @timber.io {"level":"info","dt":"2016-09-01T07:00:00.000000-05:00"}
128
+ # {"level":"info","dt":"2016-09-01T07:00:00.000000-05:00","message":"My log message"}
109
129
  #
110
- class HybridFormatter < Formatter
111
- METADATA_CALLOUT = "@timber.io".freeze
112
-
130
+ class MsgPackFormatter < Formatter
113
131
  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
132
  # use << for concatenation for performance reasons
117
- log_entry.message.gsub("\n", "\\n") << " " << METADATA_CALLOUT << " " << metadata << "\n"
133
+ build_log_entry(severity, time, progname, msg).as_json.to_msgpack << "\n"
118
134
  end
119
135
  end
120
136
 
@@ -127,7 +143,20 @@ module Timber
127
143
  # logger.formatter = Timber::Logger::JSONFormatter.new
128
144
  def initialize(*args)
129
145
  super(*args)
130
- self.formatter = HybridFormatter.new
146
+ if args.size == 1 and args.first.is_a?(LogDevices::HTTP)
147
+ self.formatter = MsgPackFormatter.new
148
+ else
149
+ self.formatter = HybridFormatter.new
150
+ end
151
+ end
152
+
153
+ def formatter=(value)
154
+ if @dev.is_a?(Timber::LogDevices::HTTP)
155
+ raise ArgumentError.new("The formatter cannot be changed when using the " +
156
+ "Timber::LogDevices::HTTP log device. The MsgPackFormatter must be used for proper " +
157
+ "delivery.")
158
+ end
159
+ super
131
160
  end
132
161
 
133
162
  # Backwards compatibility with older ActiveSupport::Logger versions
@@ -2,6 +2,7 @@ require "timber/probes/action_controller_log_subscriber"
2
2
  require "timber/probes/action_dispatch_debug_exceptions"
3
3
  require "timber/probes/action_view_log_subscriber"
4
4
  require "timber/probes/active_record_log_subscriber"
5
+ require "timber/probes/active_support_tagged_logging"
5
6
  require "timber/probes/rack_http_context"
6
7
  require "timber/probes/rails_rack_logger"
7
8
 
@@ -14,6 +15,7 @@ module Timber
14
15
  ActionDispatchDebugExceptions.insert!
15
16
  ActionViewLogSubscriber.insert!
16
17
  ActiveRecordLogSubscriber.insert!
18
+ ActiveSupportTaggedLogging.insert!
17
19
  RackHTTPContext.insert!(middleware, insert_before)
18
20
  RailsRackLogger.insert!
19
21
  end
@@ -0,0 +1,106 @@
1
+ module Timber
2
+ module Probes
3
+ # Reponsible for automatimcally tracking SQL query events in `ActiveRecord`, while still
4
+ # preserving the default log style.
5
+ class ActiveSupportTaggedLogging < Probe
6
+ module FormatterMethods
7
+ def self.included(mod)
8
+ mod.module_eval do
9
+ alias_method :_timber_original_push_tags, :push_tags
10
+ alias_method :_timber_original_pop_tags, :pop_tags
11
+
12
+ def call(severity, timestamp, progname, msg)
13
+ if is_a?(Timber::Logger::Formatter)
14
+ # Don't convert the message into a string
15
+ super(severity, timestamp, progname, msg)
16
+ else
17
+ super(severity, timestamp, progname, "#{tags_text}#{msg}")
18
+ end
19
+ end
20
+
21
+ def push_tags(*tags)
22
+ _timber_original_push_tags(*tags).tap do
23
+ if current_tags.size > 0
24
+ context = Contexts::Tags.new(values: current_tags)
25
+ CurrentContext.add(context)
26
+ end
27
+ end
28
+ end
29
+
30
+ def pop_tags(size = 1)
31
+ _timber_original_pop_tags(size).tap do
32
+ if current_tags.size == 0
33
+ CurrentContext.remove(Contexts::Tags)
34
+ else
35
+ context = Contexts::Tags.new(values: current_tags)
36
+ CurrentContext.add(context)
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
43
+
44
+ module LoggerMethods
45
+ def self.included(klass)
46
+ klass.class_eval do
47
+ alias_method :_timber_original_push_tags, :push_tags
48
+ alias_method :_timber_original_pop_tags, :pop_tags
49
+
50
+ def push_tags(*tags)
51
+ _timber_original_push_tags(*tags).tap do
52
+ if current_tags.size > 0
53
+ context = Contexts::Tags.new(values: current_tags)
54
+ CurrentContext.add(context)
55
+ end
56
+ end
57
+ end
58
+
59
+ def pop_tags(size = 1)
60
+ _timber_original_pop_tags(size).tap do
61
+ if current_tags.size == 0
62
+ CurrentContext.remove(Contexts::Tags)
63
+ else
64
+ context = Contexts::Tags.new(values: current_tags)
65
+ CurrentContext.add(context)
66
+ end
67
+ end
68
+ end
69
+
70
+ def add(severity, message = nil, progname = nil, &block)
71
+ if message.nil?
72
+ if block_given?
73
+ message = block.call
74
+ else
75
+ message = progname
76
+ progname = nil #No instance variable for this like Logger
77
+ end
78
+ end
79
+ if @logger.is_a?(Timber::Logger)
80
+ @logger.add(severity, message, progname)
81
+ else
82
+ @logger.add(severity, "#{tags_text}#{message}", progname)
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
88
+
89
+ def initialize
90
+ require "active_support/tagged_logging"
91
+ rescue LoadError => e
92
+ raise RequirementNotMetError.new(e.message)
93
+ end
94
+
95
+ def insert!
96
+ if defined?(ActiveSupport::TaggedLogging::Formatter)
97
+ return true if ActiveSupport::TaggedLogging::Formatter.include?(FormatterMethods)
98
+ ActiveSupport::TaggedLogging::Formatter.send(:include, FormatterMethods)
99
+ else
100
+ return true if ActiveSupport::TaggedLogging.include?(LoggerMethods)
101
+ ActiveSupport::TaggedLogging.send(:include, LoggerMethods)
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end
@@ -15,7 +15,7 @@ module Timber
15
15
  remote_addr: request.ip,
16
16
  request_id: request_id(env)
17
17
  )
18
- CurrentContext.instance.with(context) do
18
+ CurrentContext.with(context) do
19
19
  @app.call(env)
20
20
  end
21
21
  end
@@ -1,3 +1,3 @@
1
1
  module Timber
2
- VERSION = "1.0.3"
2
+ VERSION = "1.0.4"
3
3
  end
@@ -0,0 +1,58 @@
1
+ require "spec_helper"
2
+
3
+ describe Timber::LogDevices::HTTP::TriggeredBuffer do
4
+ describe "#write" do
5
+ it "should trigger a buffer overflow for large messages" do
6
+ buffer = described_class.new(:payload_limit_bytes => 10)
7
+ msg = "a" * 11
8
+ expect(buffer).to receive(:handle_overflow).exactly(1).times.with(msg)
9
+ buffer.write(msg)
10
+ end
11
+
12
+ it "should trigger a buffer overflow when exceeding the limit" do
13
+ buffer = described_class.new(:limit_bytes => 10)
14
+ msg = "a" * 11
15
+ expect(buffer).to receive(:handle_overflow).exactly(1).times.with(msg)
16
+ buffer.write(msg)
17
+ end
18
+
19
+ it "should start a new buffer when empty and append when not" do
20
+ buffer = described_class.new
21
+ result = buffer.write("test")
22
+ expect(result).to be_nil
23
+ expect(buffer.send(:writable_buffer)).to eq("test")
24
+ result = buffer.write("again")
25
+ expect(result).to be_nil
26
+ expect(buffer.send(:writable_buffer)).to eq("testagain")
27
+ end
28
+
29
+ it "should return the old buffer when it has exceeded it's limit" do
30
+ buffer = described_class.new(:payload_limit_bytes => 10)
31
+ msg = "a" * 6
32
+ result = buffer.write(msg)
33
+ expect(result).to be_nil
34
+ result = buffer.write(msg)
35
+ expect(result).to eq(msg)
36
+ expect(result).to be_frozen
37
+ end
38
+
39
+ it "should write a new buffer when the latest is frozen" do
40
+ buffer = described_class.new
41
+ buffer.write("test")
42
+ result = buffer.reserve
43
+ expect(result).to eq("test")
44
+ buffer.write("again")
45
+ expect(buffer.send(:writable_buffer)).to eq("again")
46
+ end
47
+ end
48
+
49
+ describe "#reserve" do
50
+ it "should reserve the latest buffer and freeze it" do
51
+ buffer = described_class.new
52
+ buffer.write("test")
53
+ result = buffer.reserve
54
+ expect(result).to eq("test")
55
+ expect(result).to be_frozen
56
+ end
57
+ end
58
+ end
@@ -1,62 +1,85 @@
1
- # require "spec_helper"
2
-
3
- # describe Timber::LogDevices::HTTP do
4
- # # We have to define our own at_exit method, because the mocks and
5
- # # everything are stripped out before. Otherwise it tries to issue
6
- # # a request :(
7
- # before(:each) do
8
- # described_class.class_eval do
9
- # def at_exit; true; end
10
- # end
11
- # end
12
-
13
- # describe "#initialize" do
14
- # it "should start a thread for delivery" do
15
- # allow_any_instance_of(described_class).to receive(:at_exit).exactly(1).times.and_return(true)
16
- # expect_any_instance_of(described_class).to receive(:deliver).exactly(2).times.and_return(true)
17
- # http = described_class.new("MYKEY", frequency_seconds: 0.1)
18
- # thread = http.instance_variable_get(:@delivery_thread)
19
- # expect(thread).to be_alive
20
- # sleep 0.25 # allow 2 iterations
21
- # http.close
22
- # end
23
- # end
24
-
25
- # describe "#write" do
26
- # let(:http) { described_class.new("MYKEY") }
27
- # let(:buffer) { http.instance_variable_get(:@buffer) }
28
-
29
- # after(:each) { http.close }
30
-
31
- # it "should buffer the messages" do
32
- # http.write("test log message")
33
- # expect(buffer.read).to eq("test log message")
34
- # end
35
- # end
36
-
37
- # describe "#deliver" do
38
- # let(:http) { described_class.new("MYKEY") }
39
- # let(:buffer) { http.instance_variable_get(:@buffer) }
40
-
41
- # after(:each) { http.close }
42
-
43
- # it "should delivery properly and flush the buffer" do
44
- # expect_any_instance_of(described_class).to receive(:at_exit).exactly(1).times.and_return(true)
45
- # stub = stub_request(:post, "https://api.timber.io/http_frames").
46
- # with(
47
- # :body => "test log message",
48
- # :headers => {'Authorization'=>'Basic TVlLRVk=', 'Connection'=>'keep-alive', 'Content-Type'=>'application/json', 'User-Agent'=>'Timber Ruby Gem/1.0.0'}
49
- # ).
50
- # to_return(:status => 200, :body => "", :headers => {})
51
-
52
- # http.write("test log message")
53
-
54
- # expect(buffer).to_not be_empty
55
-
56
- # http.send(:deliver)
57
-
58
- # expect(stub).to have_been_requested.times(1)
59
- # expect(buffer).to be_empty
60
- # end
61
- # end
62
- # end
1
+ require "spec_helper"
2
+
3
+ describe Timber::LogDevices::HTTP do
4
+ describe "#initialize" do
5
+ it "should start a thread for delivery" do
6
+ expect_any_instance_of(described_class).to receive(:deliver).at_least(1).times.and_return(true)
7
+ http = described_class.new("MYKEY", delivery_frequency_seconds: 0.1)
8
+ thread = http.instance_variable_get(:@delivery_interval_thread)
9
+ expect(thread).to be_alive
10
+
11
+ http.write("my log message")
12
+ sleep 0.3 # too fast!
13
+ end
14
+ end
15
+
16
+ describe "#write" do
17
+ let(:http) { described_class.new("MYKEY") }
18
+ let(:buffer) { http.instance_variable_get(:@buffer) }
19
+
20
+ it "should buffer the messages" do
21
+ http.write("test log message")
22
+ expect(buffer.reserve).to eq("test log message")
23
+ end
24
+
25
+ context "with a low payload limit" do
26
+ let(:http) { described_class.new("MYKEY", :payload_limit_bytes => 20) }
27
+
28
+ it "should attempt a delivery when the payload limit is exceeded" do
29
+ message = "a" * 19
30
+ http.write(message)
31
+ expect(http).to receive(:deliver).exactly(1).times.with(message)
32
+ http.write("my log message")
33
+ end
34
+ end
35
+ end
36
+
37
+ describe "#close" do
38
+ let(:http) { described_class.new("MYKEY") }
39
+
40
+ it "should kill the delivery thread the messages" do
41
+ http.close
42
+ thread = http.instance_variable_get(:@delivery_interval_thread)
43
+ sleep 0.1 # too fast!
44
+ expect(thread).to_not be_alive
45
+ end
46
+
47
+ it "should attempt a delivery" do
48
+ message = "a" * 19
49
+ http.write(message)
50
+ expect(http).to receive(:deliver).exactly(1).times.with(message)
51
+ http.close
52
+ end
53
+ end
54
+
55
+ describe "#deliver" do
56
+ let(:http) { described_class.new("MYKEY") }
57
+
58
+ after(:each) { http.close }
59
+
60
+ it "should delivery properly and flush the buffer" do
61
+ stub = stub_request(:post, "https://api.timber.io/http_frames").
62
+ with(
63
+ :body => "test log message",
64
+ :headers => {
65
+ 'Authorization' => 'Basic TVlLRVk=',
66
+ 'Connection' => 'keep-alive',
67
+ 'Content-Type' => 'application/x-timber-msgpack-frame-1',
68
+ 'User-Agent' => "Timber Ruby Gem/#{Timber::VERSION}"
69
+ }
70
+ ).
71
+ to_return(:status => 200, :body => "", :headers => {})
72
+
73
+ http.write("test log message")
74
+ buffer = http.instance_variable_get(:@buffer)
75
+ buffers = buffer.instance_variable_get(:@buffers)
76
+ expect(buffers.size).to eq(1)
77
+ body = buffer.reserve
78
+ thread = http.send(:deliver, body)
79
+ thread.join
80
+
81
+ expect(stub).to have_been_requested.times(1)
82
+ expect(buffers.size).to eq(0)
83
+ end
84
+ end
85
+ end
@@ -29,7 +29,7 @@ describe Timber::Logger, :rails_23 => true do
29
29
  end
30
30
 
31
31
  around(:each) do |example|
32
- Timber::CurrentContext.instance.with(http_context) do
32
+ Timber::CurrentContext.with(http_context) do
33
33
  example.run
34
34
  end
35
35
  end
@@ -76,13 +76,25 @@ describe Timber::Logger, :rails_23 => true do
76
76
  end
77
77
  end
78
78
 
79
- context "with TaggedLogging" do
80
- let(:logger) { ActiveSupport::TaggedLogging.new(Timber::Logger.new(io)) }
79
+ if defined?(ActiveSupport::TaggedLogging)
80
+ context "with TaggedLogging", :rails_23 => false do
81
+ let(:logger) { ActiveSupport::TaggedLogging.new(Timber::Logger.new(io)) }
81
82
 
82
- it "should format properly with events" do
83
- message = Timber::Events::SQLQuery.new(sql: "select * from users", time_ms: 56, message: "select * from users")
84
- logger.info(message)
85
- expect(io.string).to eq("select * from users @timber.io {\"level\":\"info\",\"dt\":\"2016-09-01T12:00:00.000000Z\",\"event\":{\"sql_query\":{\"sql\":\"select * from users\",\"time_ms\":56}}}\n")
83
+ it "should format properly with events" do
84
+ message = Timber::Events::SQLQuery.new(sql: "select * from users", time_ms: 56, message: "select * from users")
85
+ logger.tagged("tag") do
86
+ logger.info(message)
87
+ end
88
+ expect(io.string).to eq("select * from users @timber.io {\"level\":\"info\",\"dt\":\"2016-09-01T12:00:00.000000Z\",\"event\":{\"sql_query\":{\"sql\":\"select * from users\",\"time_ms\":56}},\"context\":{\"tags\":[\"tag\"]}}\n")
89
+ end
90
+ end
91
+ end
92
+
93
+ context "with the HTTP log device" do
94
+ let(:io) { Timber::LogDevices::HTTP.new("my_key") }
95
+
96
+ it "should use the msgpack formatter" do
97
+ expect(logger.formatter).to be_kind_of(Timber::Logger::MsgPackFormatter)
86
98
  end
87
99
  end
88
100
  end
@@ -41,11 +41,7 @@ describe Timber::Probes::RackHTTPContext do
41
41
  dispatch_rails_request("/rack_http")
42
42
  http_context = Thread.current[:_timber_context][:http]
43
43
 
44
- expect(http_context).to be_kind_of(Timber::Contexts::HTTP)
45
- expect(http_context.method).to eq("GET")
46
- expect(http_context.path).to eq("/rack_http")
47
- expect(http_context.remote_addr).to eq("123.456.789.10")
48
- expect(http_context.request_id).to_not be_nil
44
+ expect(http_context).to eq({:method=>"GET", :path=>"/rack_http", :remote_addr=>"123.456.789.10", :request_id=>"unique-request-id-1234"})
49
45
  message = "Processing by RackHttpController#index as HTML @timber.io {\"level\":\"info\",\"dt\":\"2016-09-01T12:00:00.000000Z\",\"event\":{\"controller_call\":{\"controller\":\"RackHttpController\",\"action\":\"index\"}},\"context\":{\"http\":{\"method\":\"GET\",\"path\":\"/rack_http\",\"remote_addr\":\"123.456.789.10\",\"request_id\":\"unique-request-id-1234\"}}}\nCompleted 200 OK in 0.0ms (Views: 1.0ms) @timber.io {\"level\":\"info\",\"dt\":\"2016-09-01T12:00:00.000000Z\",\"event\":{\"http_response\":{\"status\":200,\"time_ms\":0.0}},\"context\":{\"http\":{\"method\":\"GET\",\"path\":\"/rack_http\",\"remote_addr\":\"123.456.789.10\",\"request_id\":\"unique-request-id-1234\"}}}\n"
50
46
  expect(io.string).to eq(message)
51
47
  end
@@ -8,8 +8,8 @@ Gem::Specification.new do |s|
8
8
  s.platform = Gem::Platform::RUBY
9
9
  s.authors = ["Timber Technologies, Inc."]
10
10
  s.email = ["hi@timber.io"]
11
- s.homepage = "http://timber.io"
12
- s.summary = "Logs you'll actually use."
11
+ s.homepage = "https://github.com/timberio/timber-ruby"
12
+ s.summary = "Instant log gratification."
13
13
 
14
14
  s.required_ruby_version = '>= 1.9.0'
15
15
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: timber
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.3
4
+ version: 1.0.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Timber Technologies, Inc.
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-12-14 00:00:00.000000000 Z
11
+ date: 2016-12-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: msgpack
@@ -46,6 +46,7 @@ files:
46
46
  - lib/timber/contexts/custom.rb
47
47
  - lib/timber/contexts/http.rb
48
48
  - lib/timber/contexts/organization.rb
49
+ - lib/timber/contexts/tags.rb
49
50
  - lib/timber/contexts/user.rb
50
51
  - lib/timber/current_context.rb
51
52
  - lib/timber/event.rb
@@ -61,6 +62,7 @@ files:
61
62
  - lib/timber/frameworks/rails.rb
62
63
  - lib/timber/log_devices.rb
63
64
  - lib/timber/log_devices/http.rb
65
+ - lib/timber/log_devices/http/triggered_buffer.rb
64
66
  - lib/timber/log_entry.rb
65
67
  - lib/timber/logger.rb
66
68
  - lib/timber/probe.rb
@@ -72,6 +74,7 @@ files:
72
74
  - lib/timber/probes/action_view_log_subscriber/log_subscriber.rb
73
75
  - lib/timber/probes/active_record_log_subscriber.rb
74
76
  - lib/timber/probes/active_record_log_subscriber/log_subscriber.rb
77
+ - lib/timber/probes/active_support_tagged_logging.rb
75
78
  - lib/timber/probes/rack_http_context.rb
76
79
  - lib/timber/probes/rails_rack_logger.rb
77
80
  - lib/timber/util.rb
@@ -92,6 +95,7 @@ files:
92
95
  - spec/support/timecop.rb
93
96
  - spec/support/webmock.rb
94
97
  - spec/timber/events_spec.rb
98
+ - spec/timber/log_devices/http/triggered_buffer_spec.rb
95
99
  - spec/timber/log_devices/http_spec.rb
96
100
  - spec/timber/logger_spec.rb
97
101
  - spec/timber/probes/action_controller_log_subscriber_spec.rb
@@ -101,7 +105,7 @@ files:
101
105
  - spec/timber/probes/rack_http_context_spec.rb
102
106
  - spec/timber/probes/rails_rack_logger_spec.rb
103
107
  - timber.gemspec
104
- homepage: http://timber.io
108
+ homepage: https://github.com/timberio/timber-ruby
105
109
  licenses: []
106
110
  metadata: {}
107
111
  post_install_message:
@@ -123,7 +127,7 @@ rubyforge_project:
123
127
  rubygems_version: 2.6.8
124
128
  signing_key:
125
129
  specification_version: 4
126
- summary: Logs you'll actually use.
130
+ summary: Instant log gratification.
127
131
  test_files:
128
132
  - spec/spec_helper.rb
129
133
  - spec/support/action_controller.rb
@@ -139,6 +143,7 @@ test_files:
139
143
  - spec/support/timecop.rb
140
144
  - spec/support/webmock.rb
141
145
  - spec/timber/events_spec.rb
146
+ - spec/timber/log_devices/http/triggered_buffer_spec.rb
142
147
  - spec/timber/log_devices/http_spec.rb
143
148
  - spec/timber/logger_spec.rb
144
149
  - spec/timber/probes/action_controller_log_subscriber_spec.rb