timber 1.0.3 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml 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