l2meter 0.11.0 → 0.15.1

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
- SHA1:
3
- metadata.gz: e11fd798f796a3a70084ef2989c0f3d233d55d7f
4
- data.tar.gz: 57141bec2eaee516c4913432720d653f52c3009a
2
+ SHA256:
3
+ metadata.gz: 3863cb0c00c39be864ceba6afde2364cc8ce88d5c2bbeeaa05c1525110c8ef92
4
+ data.tar.gz: 9320a7a9e92487406a3bc81d7d784d7c92b6735203a65c368cc8ad7ce69aa007
5
5
  SHA512:
6
- metadata.gz: 7f0b4f401895a9b09705d488b96451e8f965d862f24f60255a4db401d2ef0f8b87dc45b960815cd3c64878c28cf63fcbdd43c4124e714e0ef39ccd3876417a51
7
- data.tar.gz: ff395e97f4c500d9bd3cada7b0e4ed68af6ae1d8f23eb1bacc16c58217b6cabd197d185b6c3950042e2f254344c7f917bcbe0322a2307a9526d3c916cb77e91e
6
+ metadata.gz: f40523f9ac4d79e603c568833f82a94945c6086bf96a8b64bd5125594f98b094274230bb0c65d1c01d59c9a16cca4412e9862ccf29c37aeec1a83dfafc816b87
7
+ data.tar.gz: 80c5ad06795a01788c6c07cb4ec29454bc39d61e6fe369e0e07ff1248b1f23d0d4acd52483f051d3e39d474e93e2f7eb9e897391b33ceaf25f700704cc1ca710
@@ -0,0 +1,20 @@
1
+ # Ignore all logfiles and tempfiles
2
+ /log/
3
+ /tmp/
4
+ /tags
5
+ .byebug_history
6
+
7
+ # Package and dependency caches
8
+ /.bundle
9
+ /Gemfile.lock
10
+ /pkg/
11
+
12
+ # Generated documentation
13
+ /doc/
14
+ /.yardoc
15
+ /_yardoc/
16
+
17
+ # Spec reports and failure tracking
18
+ /coverage/
19
+ /spec/rspec-status.txt
20
+ /spec/reports/
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --require spec_helper
2
+ --color
3
+ --format progress
@@ -0,0 +1,6 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.7
4
+ - 2.6
5
+ - 2.5
6
+ - 2.4
@@ -0,0 +1,34 @@
1
+ # Changelog
2
+ All notable changes to this project will be documented in this file.
3
+
4
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
5
+ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## [Unreleased]
8
+
9
+ ## [0.15.1] 2020-07-30
10
+
11
+ ### Fixed
12
+ - Fix Ruby 2.7 Proc.new warning ([#10](https://github.com/heroku/l2meter/pull/10))
13
+
14
+ ## [0.15.0] 2020-05-15
15
+
16
+ ### Added
17
+ - Allow outputting of true/false/nil values via a config option. ([#9](https://github.com/heroku/l2meter/pull/9))
18
+
19
+ ## [0.14.0] 2020-04-16
20
+
21
+ ### Added
22
+ - CHANGELOG.md
23
+
24
+ ### Changed
25
+ - Officially only support Ruby 2.5+; all older Rubies are EoL'd.
26
+
27
+ ### Fixed
28
+ - Fix Ruby 2.7 proc warning
29
+ - Fix Ruby 2.7 last argument as keyword params warning
30
+
31
+ ## [0.13.0] 2019-05-11
32
+
33
+ ### Changed
34
+ - Total re-write to fix memory leaks in `L2meter::ThreadSafe` proxy object. See: https://github.com/heroku/l2meter/issues/6
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source "https://rubygems.org"
2
+
3
+ gemspec
data/README.md CHANGED
@@ -1,49 +1,50 @@
1
1
  # L2meter
2
2
  [![Gem Version](https://img.shields.io/gem/v/l2meter.svg)](https://rubygems.org/gems/l2meter)
3
- [![Build Status](https://img.shields.io/travis/rwz/l2meter.svg)](http://travis-ci.org/rwz/l2meter)
4
- [![Code Climate](https://img.shields.io/codeclimate/github/rwz/l2meter.svg)](https://codeclimate.com/github/rwz/l2meter)
3
+ [![Build Status](https://travis-ci.com/heroku/l2meter.svg?branch=main)](http://travis-ci.com/heroku/l2meter)
5
4
 
6
- L2meter is a little gem that helps you build loggers that outputs things in
7
- l2met-friendly format.
5
+ L2meter is a little gem for building [logfmt]-compatiable loggers.
6
+
7
+ [logfmt]: https://www.brandur.org/logfmt
8
8
 
9
9
  ### Basics
10
10
 
11
11
  A new logger might be created like so:
12
12
 
13
13
  ```ruby
14
- Metrics = L2meter.build
14
+ logger = L2meter.build
15
15
  ```
16
16
 
17
- If you plan to use it globally across different components of your app,consider
18
- making it constant.
17
+ Consider making the logger a constant to make it easier to use across different
18
+ components of the app or globally.
19
19
 
20
- The base `log` method accepts two type of things: bare values and key-value
20
+ The base `log` method accepts two type of arguments: bare values and key-value
21
21
  pairs in form of hashes.
22
22
 
23
23
  ```ruby
24
- Metrics.log "Hello world" # => hello-world
25
- Metrics.log :db_query, result: :success # => db-query result=success
24
+ logger.log "Hello world" # => hello-world
25
+ logger.log :db_query, result: :success # => db-query result=success
26
26
  ```
27
27
 
28
- It can also take a block. In this case the message will be emitted twice, once
29
- at the start of the execution and another at the end. The end result might look
30
- like so:
28
+ The method also takes a block. In this case the message will be emitted twice,
29
+ once at the start of the execution and once at the end. The end result might
30
+ look like so:
31
31
 
32
32
  ```ruby
33
- Metrics.log :doing_work do # => doing-work at=start
34
- do_some_work #
35
- Metrics.log :work_done # => work-done
36
- end # => doing-work at=finish elapsed=1.2345
33
+ logger.log :doing_work do # => doing-work at=start
34
+ do_some_work #
35
+ logger.log :work_done # => work-done
36
+ end # => doing-work at=finish elapsed=1.2345
37
37
  ```
38
38
 
39
- In case the exception is raised inside the block, l2meter will report is like
40
- so:
39
+ In case of an exception inside the block, all relevant information is logged
40
+ and then the exception is re-raised.
41
41
 
42
42
  ```ruby
43
- Metrics.log :doing_work do # => doing-work at=start
43
+ logger.log :doing_work do # => doing-work at=start
44
44
  raise ArgumentError, \ #
45
45
  "something is wrong" #
46
46
  end # => doing-work at=exception exception=ArgumentError message="something is wrong" elapsed=1.2345
47
+ # ArgumentError: something is wrong
47
48
  ```
48
49
 
49
50
  ## Context
@@ -54,7 +55,7 @@ L2meter allows setting context for a block. It might work something like this:
54
55
  def do_work_with_retries
55
56
  attempt = 1
56
57
  begin
57
- Metrics.context attempt: attempt do
58
+ logger.context attempt: attempt do
58
59
  do_some_work # => doing-work attempt=1
59
60
  # => doing-work attempt=2
60
61
  # => doing-work attempt=3
@@ -69,12 +70,12 @@ end
69
70
  L2meter supports dynamic contexts as well. You can pass a proc instead of raw
70
71
  value in order to use it.
71
72
 
72
- The same example as above could be re-written like this instead:
73
+ The example above could be re-written like this instead:
73
74
 
74
75
  ```ruby
75
76
  def do_work_with_retries
76
77
  attempt = 1
77
- Metrics.context ->{{ attempt: attempt }} do
78
+ logger.context ->{{ attempt: attempt }} do
78
79
  begin
79
80
  do_some_work
80
81
  rescue => error
@@ -85,162 +86,189 @@ def do_work_with_retries
85
86
  end
86
87
  ```
87
88
 
88
- ## Contexted logging
89
-
90
- Sometimes you want another copy of the logger with a specific context on it.
91
- You can create one like so:
89
+ It's possible to create a dedicated copy of the logger with some specific
90
+ context attached to it.
92
91
 
93
92
  ```ruby
94
- logger = Metrics.context(:super_worker, username: "joe")
93
+ worker_logger = logger.context(component: :worker, worker_id: 123)
95
94
 
96
- SuperWorker.new(logger: logger).run # => super-worker username=joe some-other=superworker-output
95
+ MyWorker.new(logger: worker_logger).run # => component=worker worker_id=123 status="doing work"
97
96
  ```
98
97
 
99
98
  ## Batching
100
99
 
101
- There's also a way to batch several calls into a single log line:
100
+ There's a way to batch several calls into a single log line:
102
101
 
103
102
  ```ruby
104
- Metrics.batch do
105
- Metrics.log foo: :bar
106
- Metrics.unique :registeration, "user@example.com"
107
- Metrics.count :thing, 10
108
- Metrics.sample :other_thing, 20
103
+ logger.batch do
104
+ logger.log foo: :bar
105
+ logger.unique :registration, "user@example.com"
106
+ logger.count :thing, 10
107
+ logger.sample :other_thing, 20
109
108
  end # => foo=bar unique#registration=user@example.com count#thing=10 sample#other-thing=20
110
109
  ```
111
110
 
112
- ## Other
111
+ ## Metrics
113
112
 
114
- Some other l2met-specific methods are supported.
113
+ Some [l2met]-specific metrics are supported.
114
+
115
+ [l2met]: https://r.32k.io/l2met-introduction
115
116
 
116
117
  ```ruby
117
- Metrics.count :user_registered # => count#user-registered=1
118
- Metrics.count :registered_users, 10 # => count#registered-users=10
118
+ logger.count :user_registered # => count#user-registered=1
119
+ logger.count :registered_users, 10 # => count#registered-users=10
119
120
 
120
- Metrics.measure :connection_count, 20 # => measure#connection-count=20
121
- Metrics.measure :db_query, 235, unit: :ms, # => measure#db-query.ms=235
121
+ logger.measure :connection_count, 20 # => measure#connection-count=20
122
+ logger.measure :db_query, 235, unit: :ms, # => measure#db-query.ms=235
122
123
 
123
- Metrics.sample :connection_count, 20, # => sample#connection-count=235
124
- Metrics.sample :db_query, 235, unit: :ms, # => sample#db-query.ms=235
124
+ logger.sample :connection_count, 20, # => sample#connection-count=235
125
+ logger.sample :db_query, 235, unit: :ms, # => sample#db-query.ms=235
125
126
 
126
- Metrics.unique :user, "bob@example.com" # => unique#user=bob@example.com
127
+ logger.unique :user, "bob@example.com" # => unique#user=bob@example.com
127
128
  ```
128
129
 
129
- L2meter also allows to append elapsed time to your log messages automatically.
130
+ ## Measuring Time
131
+
132
+ L2meter allows to append elapsed time to log messages automatically.
130
133
 
131
134
  ```ruby
132
- Metrics.with_elapsed do
135
+ logger.with_elapsed do
133
136
  do_work_step_1
134
- Metrics.log :step_1_done # => step-1-done elapsed=1.2345
137
+ logger.log :step_1_done # => step-1-done elapsed=1.2345
135
138
  do_work_step_2
136
- Metrics.log :step_2_done # => step-2-done elapsed=2.3456
139
+ logger.log :step_2_done # => step-2-done elapsed=2.3456
137
140
  end
138
141
  ```
139
142
 
140
- ### Configuration
143
+ ## Configuration
141
144
 
142
- L2meter supports configuration. Here's how you can configure things:
145
+ L2meter supports customizable configuration.
143
146
 
144
147
  ```ruby
145
- Metrics = L2meter.build do |config|
148
+ logger = L2meter.build do |config|
146
149
  # configuration happens here
147
150
  end
148
151
  ```
149
152
 
150
- Here's the list of all configurable things:
153
+ Here's the full list of available settings.
151
154
 
152
- #### Global context
155
+ ### Global context
153
156
 
154
- Global context works similary to context method, but globally:
157
+ Global context works similarly to context method, but globally:
155
158
 
156
159
  ```ruby
157
160
  config.context = { app_name: "my-app-name" }
158
161
 
159
162
  # ...
160
163
 
161
- Metrics.log foo: :bar # => app-name=my-app-name foo=bar
164
+ logger.log foo: :bar # => app-name=my-app-name foo=bar
162
165
  ```
163
166
 
164
167
  Dynamic context is also supported:
165
168
 
166
169
  ```ruby
167
170
  config.context do
168
- { request_id: CurrentContext.request_id }
171
+ { request_id: SecureRandom.uuid }
169
172
  end
173
+
174
+ logger.log :hello # => hello request_id=4209ba28-4a7c-40d6-af69-c2c1ddf51f19
175
+ logger.log :world # => world request_id=b6836b1b-5710-4f5f-926d-91ab9988a7c1
170
176
  ```
171
177
 
172
- #### Sorting
178
+ ### Sorting
173
179
 
174
180
  By default l2meter doesn't sort tokens before output, putting them in the order
175
- they're passed. But you can make it sorted like so:
181
+ they're passed. But it's possible to sort them like so:
176
182
 
177
183
  ```ruby
178
184
  config.sort = true
179
185
 
180
186
  # ...
181
187
 
182
- Metrics.log :c, :b, :a # => a b c
188
+ logger.log :c, :b, :a # => a b c
183
189
  ```
184
190
 
185
- #### Source
191
+ ### Source
186
192
 
187
193
  Source is a special parameter that'll be appended to all emitted messages.
188
194
 
189
195
  ```ruby
190
- config.source = "production"
196
+ config.source = "com.heroku.my-application.staging"
191
197
 
192
198
  # ...
193
199
 
194
- Metrics.log foo: :bar # => source=production foo=bar
200
+ logger.log foo: :bar # => source=com.heroku.my-application.staging foo=bar
195
201
  ```
196
202
 
197
- #### Prefix
203
+ ### Prefix
198
204
 
199
- Prefix allows namespacing your measure/count/unique/sample calls.
205
+ Prefix allows to add namespacing to measure/count/unique/sample calls.
200
206
 
201
207
  ```ruby
202
208
  config.prefix = "my-app"
203
209
 
204
210
  # ...
205
211
 
206
- Metrics.count :users, 100500 # => count#my-app.users=100500
212
+ logger.count :users, 100500 # => count#my-app.users=100500
207
213
  ```
208
214
 
209
- ## Scrubbing
215
+ ### Scrubbing
210
216
 
211
217
  L2meter allows plugging in custom scrubbing logic that might be useful in
212
- environments where logging compliance is important.
218
+ environments where logging compliance is important to prevent accidentally
219
+ leaking sensitive information.
213
220
 
214
221
  ```ruby
215
- config.scrubber = ->(key, value) do
222
+ config.scrubber = -> (key, value) do
216
223
  begin
217
224
  uri = URI.parse(value)
218
- uri.password = "scrubbed" if uri.password
225
+ uri.password = "redacted" if uri.password
219
226
  uri.to_s
220
227
  rescue URI::Error
221
228
  value
222
229
  end
223
230
  end
224
231
 
225
- Metric.log my_url: "https://user:password@example.com"
232
+ logger.log my_url: "https://user:password@example.com"
226
233
  # => my-url="https://user:redacted@example.com"
227
234
  ```
228
235
 
229
236
  Note that returning nil value will make l2meter omit the field completely.
230
237
 
238
+ ### "Compacting" values
239
+
240
+ By default l2meter will treat key-value pairs where the value is `true`, `false` or `nil` differently. `false` and `nil` values will cause the whole pair to be omitted, `true` will cause just the key to be output:
241
+
242
+ ```ruby
243
+ logger.log foo: "hello", bar: true # => foo=hello bar
244
+ logger.log foo: "hello", bar: false # => foo=hello
245
+ logger.log foo: "hello", bar: nil # => foo=hello
246
+ ```
247
+
248
+ When the option is disabled, the full pairs are emitted:
249
+
250
+ ```ruby
251
+ config.compact_values = false
252
+ logger.log foo: "hello", bar: true # => foo=hello bar=true
253
+ logger.log foo: "hello", bar: false # => foo=hello bar=false
254
+ logger.log foo: "hello", bar: nil # => foo=hello bar=null
255
+ ```
256
+
257
+ Note that "null" is output in the `nil` case.
258
+
231
259
  ## Silence
232
260
 
233
- There's a way to temporary silence the log emitter. This might be userful for
261
+ There's a way to temporary silence the log emitter. This might be useful for
234
262
  tests for example.
235
263
 
236
264
  ```ruby
237
- Metrics.silence do
265
+ logger.silence do
238
266
  # logger is completely silenced
239
- Metrics.log "hello world" # nothing is emitted here
267
+ logger.log "hello world" # nothing is emitted here
240
268
  end
241
269
 
242
270
  # works normally again
243
- Metrics.log :foo # => foo
271
+ logger.log :foo # => foo
244
272
  ```
245
273
 
246
274
  The typical setup for RSpec might look like this:
@@ -248,16 +276,17 @@ The typical setup for RSpec might look like this:
248
276
  ```ruby
249
277
  RSpec.configure do |config|
250
278
  config.around :each do |example|
251
- Metrics.silence &example
279
+ MyLogger.silence &example
252
280
  end
253
281
  end
254
282
  ```
255
283
 
256
- Note that this code will only silence logger in the current thread. It'll still
257
- produce output if you fire up a new thread. To silence it completely, use
258
- `disable!` method, like so:
284
+ Note that silence method will only suppress logging in the current thread.
285
+ It'll still produce output if you fire up a new thread. To silence it
286
+ completely, use `disable!` method. This will completely silence the logger
287
+ across all threads.
259
288
 
260
289
  ```ruby
261
290
  # spec/spec_helper.rb
262
- Metrics.disable!
291
+ MyLogger.disable!
263
292
  ```
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task default: :spec
@@ -0,0 +1,35 @@
1
+ require File.expand_path("../lib/l2meter/version", __FILE__)
2
+
3
+ Gem::Specification.new do |spec|
4
+ spec.name = "l2meter"
5
+ spec.version = L2meter::VERSION
6
+ spec.authors = ["Pavel Pravosud"]
7
+ spec.email = ["pavel@pravosud.com"]
8
+
9
+ spec.summary = "L2met friendly log formatter"
10
+ spec.description = "L2meter is a tool for building logfmt-compatiable loggers."
11
+ spec.homepage = "https://github.com/heroku/l2meter"
12
+ spec.license = "MIT"
13
+
14
+ spec.metadata = {
15
+ "homepage_uri" => spec.homepage,
16
+ "source_code_uri" => spec.homepage,
17
+ "bug_tracker_uri" => "#{spec.homepage}/issues",
18
+ "changelog_uri" => "#{spec.homepage}/blob/main/CHANGELOG.md"
19
+ }
20
+
21
+ # Specify which files should be added to the gem when it is released.
22
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
23
+ spec.files = Dir.chdir(File.expand_path("..", __FILE__)) do
24
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
25
+ end
26
+ spec.bindir = "exe"
27
+ spec.executables = []
28
+ spec.require_paths = ["lib"]
29
+
30
+ spec.add_development_dependency "bundler"
31
+ spec.add_development_dependency "pry-byebug"
32
+ spec.add_development_dependency "rake"
33
+ spec.add_development_dependency "rspec", "~> 3.8.0"
34
+ spec.add_development_dependency "timecop"
35
+ end
@@ -4,13 +4,11 @@ module L2meter
4
4
  extend self
5
5
 
6
6
  autoload :Configuration, "l2meter/configuration"
7
- autoload :Emitter, "l2meter/emitter"
8
- autoload :NullObject, "l2meter/null_object"
9
- autoload :ThreadSafe, "l2meter/thread_safe"
7
+ autoload :Emitter, "l2meter/emitter"
8
+ autoload :NullOutput, "l2meter/null_output"
10
9
 
11
10
  def build(configuration: Configuration.new)
12
- yield configuration if block_given?
13
- emitter = Emitter.new(configuration: configuration.freeze)
14
- ThreadSafe.new(emitter)
11
+ yield(configuration) if block_given?
12
+ Emitter.new(configuration: configuration.freeze)
15
13
  end
16
14
  end
@@ -1,11 +1,11 @@
1
1
  module L2meter
2
2
  class Configuration
3
- attr_writer :output
3
+ attr_writer :context, :output
4
4
  attr_accessor :source, :prefix, :float_precision, :scrubber
5
- attr_reader :context, :key_formatter, :output
5
+ attr_reader :key_formatter, :output
6
6
 
7
7
  DEFAULT_KEY_FORMATTER = ->(key) do
8
- key.to_s.strip.downcase.gsub(/[^-a-z\d.#]+/, ?-)
8
+ key.to_s.strip.downcase.gsub(/[^-a-z\d.#]+/, "-")
9
9
  end
10
10
 
11
11
  private_constant :DEFAULT_KEY_FORMATTER
@@ -15,6 +15,8 @@ module L2meter
15
15
  @key_formatter = DEFAULT_KEY_FORMATTER
16
16
  @output = $stdout
17
17
  @float_precision = 4
18
+ @context = nil
19
+ @compact_values = true
18
20
  end
19
21
 
20
22
  def format_keys(&block)
@@ -29,14 +31,20 @@ module L2meter
29
31
  @sort = !!value
30
32
  end
31
33
 
32
- def context
33
- if block_given?
34
- @context = Proc.new
34
+ def compact_values?
35
+ @compact_values
36
+ end
37
+
38
+ def compact_values=(value)
39
+ @compact_values = !!value
40
+ end
41
+
42
+ def context(&block)
43
+ if block
44
+ @context = block
35
45
  else
36
46
  @context
37
47
  end
38
48
  end
39
-
40
- attr_writer :context
41
49
  end
42
50
  end
@@ -4,230 +4,307 @@ module L2meter
4
4
  class Emitter
5
5
  attr_reader :configuration
6
6
 
7
+ BARE_VALUE_SENTINEL = Object.new.freeze
8
+
7
9
  def initialize(configuration: Configuration.new)
8
10
  @configuration = configuration
9
- @buffer = {}
10
- @autoflush = true
11
- @contexts = []
12
- @outputs = []
13
11
  end
14
12
 
15
- def log(*args)
16
- merge! *current_contexts, *args
13
+ def log(*args, &block)
14
+ merge!(current_context, *args)
17
15
 
18
- if block_given?
19
- wrap &proc
16
+ if block
17
+ wrap(&block)
20
18
  else
21
19
  write
22
20
  end
23
21
  end
24
22
 
25
- def with_elapsed(start_time = Time.now, &block)
26
- context(elapsed_context(start_time), &block)
23
+ def context(*context_data, &block)
24
+ if block
25
+ wrap_context(context_data, &block)
26
+ else
27
+ contexted(context_data)
28
+ end
27
29
  end
28
30
 
29
- def with_output(output)
30
- @outputs.push output
31
- yield
32
- ensure
33
- @outputs.pop
31
+ def with_elapsed
32
+ context elapsed: elapse do
33
+ yield
34
+ end
34
35
  end
35
36
 
36
- def silence
37
- with_output(NullObject.new, &proc)
37
+ def silence(&block)
38
+ with_output(NullOutput.new, &block)
38
39
  end
39
40
 
40
41
  def silence!
41
- @outputs.push NullObject.new
42
+ set_output(NullOutput.new)
42
43
  end
43
44
 
44
45
  def unsilence!
45
- @outputs.pop
46
+ set_output(nil)
47
+ end
48
+
49
+ def with_output(new_output)
50
+ old_output = output
51
+ set_output(new_output)
52
+ yield
53
+ ensure
54
+ set_output(old_output)
55
+ end
56
+
57
+ def batch
58
+ old_state = in_batch?
59
+ in_batch!
60
+ yield
61
+ ensure
62
+ reset_in_batch(old_state)
63
+ write
46
64
  end
47
65
 
48
- def measure(metric, value, unit: nil)
49
- log_with_prefix :measure, metric, value, unit: unit
66
+ def measure(metric, value, **args)
67
+ log_with_prefix(:measure, metric, value, **args)
50
68
  end
51
69
 
52
- def sample(metric, value, unit: nil)
53
- log_with_prefix :sample, metric, value, unit: unit
70
+ def sample(metric, value, **args)
71
+ log_with_prefix(:sample, metric, value, **args)
54
72
  end
55
73
 
56
74
  def count(metric, value = 1)
57
- log_with_prefix :count, metric, value
75
+ log_with_prefix(:count, metric, value)
58
76
  end
59
77
 
60
78
  def unique(metric, value)
61
- log_with_prefix :unique, metric, value
79
+ log_with_prefix(:unique, metric, value)
62
80
  end
63
81
 
64
- def context(*context_data)
65
- return clone_with_context(context_data) unless block_given?
66
- push_context context_data
67
- yield
68
- ensure
69
- context_data.length.times { @contexts.pop } if block_given?
82
+ def clone
83
+ original_contexts = dynamic_contexts
84
+ original_output = output
85
+ self.class.new(configuration: configuration).tap do |clone|
86
+ clone.instance_eval do
87
+ dynamic_contexts.concat(original_contexts)
88
+ set_output original_output
89
+ end
90
+ end
70
91
  end
71
92
 
72
- def clone
73
- cloned_contexts = @contexts.clone
74
- cloned_outputs = @outputs.clone
75
- self.class.new(configuration: configuration).instance_eval do
76
- @contexts = cloned_contexts
77
- @outputs = cloned_outputs
78
- self
93
+ private
94
+
95
+ def log_with_prefix(method, key, value, unit: nil)
96
+ key = [configuration.prefix, key, unit].compact.join(".")
97
+ log(Hash["#{method}##{key}", value])
98
+ end
99
+
100
+ def elapse(since = Time.now)
101
+ -> { Time.now - since }
102
+ end
103
+
104
+ def write(*args)
105
+ merge!(*args)
106
+ fire! unless in_batch?
107
+ end
108
+
109
+ def wrap(&block)
110
+ elapsed = elapse
111
+ cloned_buffer = buffer.clone
112
+ write(at: :start)
113
+ result, exception = capture(&block)
114
+ merge!(cloned_buffer)
115
+ if exception
116
+ write(unwrap_exception(exception), elapsed: elapsed)
117
+ raise(exception)
118
+ else
119
+ write(at: :finish, elapsed: elapsed)
120
+ result
79
121
  end
80
122
  end
81
123
 
82
- def batch
83
- @autoflush = false
124
+ def capture
125
+ [yield, nil]
126
+ rescue Object => exception
127
+ [nil, exception]
128
+ end
129
+
130
+ def wrap_context(context_data)
131
+ dynamic_contexts.concat(context_data)
84
132
  yield
85
133
  ensure
86
- @autoflush = true
87
- fire!
134
+ context_data.each { dynamic_contexts.pop }
88
135
  end
89
136
 
90
- def merge!(*args)
91
- @buffer.merge! format_keys(unwrap(args))
137
+ def contexted(context_data)
138
+ clone.instance_eval do
139
+ dynamic_contexts.concat(context_data)
140
+ self
141
+ end
92
142
  end
93
143
 
94
- def fire!
95
- tokens = @buffer.map { |key, value| build_token(key, value) }
96
- tokens.compact!
97
- tokens.sort! if configuration.sort?
144
+ def unwrap_exception(exception)
145
+ {
146
+ at: :exception,
147
+ exception: exception.class,
148
+ message: exception.message
149
+ }
150
+ end
98
151
 
99
- output_queue.last.print tokens.join(SPACE) << NL if tokens.any?
100
- ensure
101
- @buffer.clear
152
+ def current_context
153
+ unwrap(resolved_contexts)
102
154
  end
103
155
 
104
- protected
156
+ def current_contexts
157
+ [
158
+ source_context,
159
+ configuration.context,
160
+ *dynamic_contexts
161
+ ].compact
162
+ end
105
163
 
106
- def push_context(context_data)
107
- @contexts.concat context_data
164
+ def source_context
165
+ configuration.source ? {source: configuration.source} : {}
108
166
  end
109
167
 
110
- private
168
+ def resolved_contexts
169
+ current_contexts.map { |c| Proc === c ? c.call : c }
170
+ end
171
+
172
+ def fire!
173
+ tokens = buffer.map { |k, v| build_token(k, v) }.compact
174
+ tokens.sort! if configuration.sort?
175
+ return if tokens.empty?
176
+ output.print(tokens.join(SPACE) << NL)
177
+ ensure
178
+ buffer.clear
179
+ end
111
180
 
112
181
  SPACE = " ".freeze
113
- NL = "\n".freeze
182
+ NL = "\n".freeze
114
183
 
115
184
  private_constant :SPACE, :NL
116
185
 
117
- def unwrap(args)
118
- args.each_with_object({}) do |context, result|
119
- next if context.nil?
120
- context = Hash[context, true] unless Hash === context
121
- result.merge! context
122
- end
186
+ def scrub_value(key, value)
187
+ scrubber = configuration.scrubber
188
+ scrubber ? scrubber.call(key, value) : value
123
189
  end
124
190
 
125
191
  def build_token(key, value)
126
- value = value.call if Proc === value
127
- return if value.nil?
128
- value = scrub_value(key, value)
129
- return if value.nil?
130
- value == true ? key : "#{key}=#{format_value(value)}"
131
- end
132
-
133
- def format_float(value, unit: nil)
134
- "%.#{configuration.float_precision}f#{unit}" % value
135
- end
136
-
137
- def clone_with_context(context)
138
- clone.tap do |emitter|
139
- emitter.push_context context
192
+ case value
193
+ when Proc
194
+ build_token(key, value.call)
195
+ else
196
+ value = scrub_value(key, value)
197
+ format_token(key, value)
140
198
  end
141
199
  end
142
200
 
143
- def current_contexts
144
- contexts_queue.map do |context|
145
- context = context.call if context.respond_to?(:call)
146
- context
201
+ def format_token(key, value)
202
+ case
203
+ when value == true && configuration.compact_values?
204
+ key
205
+ when !value && configuration.compact_values?
206
+ nil
207
+ when value == BARE_VALUE_SENTINEL
208
+ key
209
+ else
210
+ value = format_value(value)
211
+ "#{key}=#{value}"
147
212
  end
148
213
  end
149
214
 
150
215
  def format_value(value)
151
216
  case value
152
- when /[^\w,.:@\-\]\[]/
153
- value.strip.gsub(/\s+/, " ").inspect
154
- when String
155
- value.to_s
156
217
  when Float
157
- format_float(value)
218
+ format_float_value(value)
219
+ when String
220
+ format_string_value(value)
158
221
  when Time
159
- value.iso8601
160
- when Hash
161
- format_value(value.inspect)
222
+ format_time_value(value)
162
223
  when Array
163
- value.map(&method(:format_value)).join(?,)
224
+ value.map(&method(:format_value)).join(",")
225
+ when nil
226
+ "null"
164
227
  else
165
228
  format_value(value.to_s)
166
229
  end
167
230
  end
168
231
 
169
- def format_keys(hash)
170
- hash.each_with_object({}) do |(key, value), acc|
171
- key = configuration.key_formatter.call(key)
172
- acc[key] = value
232
+ def format_time_value(value)
233
+ value.iso8601
234
+ end
235
+
236
+ def format_float_value(value)
237
+ format = "%.#{configuration.float_precision}f"
238
+ sprintf(format, value)
239
+ end
240
+
241
+ def format_string_value(value)
242
+ /[^\w,.:@\-\]\[]/.match?(value) ?
243
+ value.strip.gsub(/\s+/, " ").inspect :
244
+ value.to_s
245
+ end
246
+
247
+ def merge!(*args)
248
+ unwrap(args.compact).each do |key, value|
249
+ key = format_key(key)
250
+ buffer[key] = value
173
251
  end
174
252
  end
175
253
 
176
- def write(params = nil)
177
- merge! params
178
- fire! if @autoflush
254
+ def format_key(key)
255
+ configuration.key_formatter.call(key)
179
256
  end
180
257
 
181
- def log_with_prefix(method, key, value, unit: nil)
182
- key = [configuration.prefix, key, unit].compact * ?.
183
- log Hash["#{method}##{key}", value]
184
- end
185
-
186
- def wrap
187
- start_time = Time.now
188
- params = @buffer.clone
189
- write at: :start
190
- result = exception = nil
191
-
192
- begin
193
- result = yield
194
- merge! params, at: :finish
195
- rescue Object => exception
196
- merge! params, \
197
- at: :exception,
198
- exception: exception.class,
199
- message: exception.message
258
+ def unwrap(args)
259
+ {}.tap do |result|
260
+ args.each do |arg|
261
+ next if arg.nil?
262
+ arg = Hash[arg, BARE_VALUE_SENTINEL] unless Hash === arg
263
+ arg.each do |key, value|
264
+ result[key] = value
265
+ end
266
+ end
200
267
  end
268
+ end
201
269
 
202
- write elapsed_context(start_time)
270
+ def thread_state
271
+ @mutex ||= Mutex.new
272
+ @mutex.synchronize do
273
+ @threads ||= {}
203
274
 
204
- raise exception if exception
275
+ # cleaning up state from dead threads
276
+ @threads.delete_if { |t, _| !t.alive? }
205
277
 
206
- result
278
+ @threads[Thread.current] ||= {}
279
+ end
207
280
  end
208
281
 
209
- def contexts_queue
210
- [configuration.context, source_context, *@contexts].compact
282
+ def buffer
283
+ thread_state[:buffer] ||= {}
211
284
  end
212
285
 
213
- def output_queue
214
- [configuration.output, *@outputs].compact
286
+ def dynamic_contexts
287
+ thread_state[:dynamic_contexts] ||= []
215
288
  end
216
289
 
217
- def source_context
218
- { source: configuration.source }
290
+ def output
291
+ thread_state[:output] ||= configuration.output
219
292
  end
220
293
 
221
- def elapsed_context(since = Time.now)
222
- { elapsed: -> { Time.now - since } }
294
+ def set_output(new_output)
295
+ thread_state[:output] = new_output
223
296
  end
224
297
 
225
- def scrub_value(key, value)
226
- if scrubber = configuration.scrubber
227
- scrubber.call(key, value)
228
- else
229
- value
230
- end
298
+ def in_batch?
299
+ !!thread_state[:in_batch]
300
+ end
301
+
302
+ def in_batch!
303
+ reset_in_batch(true)
304
+ end
305
+
306
+ def reset_in_batch(new_value)
307
+ thread_state[:in_batch] = new_value
231
308
  end
232
309
  end
233
310
  end
@@ -0,0 +1,6 @@
1
+ module L2meter
2
+ class NullOutput
3
+ def print(*)
4
+ end
5
+ end
6
+ end
@@ -1,3 +1,3 @@
1
1
  module L2meter
2
- VERSION = "0.11.0".freeze
2
+ VERSION = "0.15.1".freeze
3
3
  end
metadata CHANGED
@@ -1,34 +1,114 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: l2meter
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.11.0
4
+ version: 0.15.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Pavel Pravosud
8
8
  autorequire:
9
- bindir: bin
9
+ bindir: exe
10
10
  cert_chain: []
11
- date: 2017-04-26 00:00:00.000000000 Z
12
- dependencies: []
13
- description:
11
+ date: 2020-07-30 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: pry-byebug
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: 3.8.0
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: 3.8.0
69
+ - !ruby/object:Gem::Dependency
70
+ name: timecop
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ description: L2meter is a tool for building logfmt-compatiable loggers.
14
84
  email:
15
85
  - pavel@pravosud.com
16
86
  executables: []
17
87
  extensions: []
18
88
  extra_rdoc_files: []
19
89
  files:
90
+ - ".gitignore"
91
+ - ".rspec"
92
+ - ".travis.yml"
93
+ - CHANGELOG.md
94
+ - Gemfile
20
95
  - LICENSE.txt
21
96
  - README.md
97
+ - Rakefile
98
+ - l2meter.gemspec
22
99
  - lib/l2meter.rb
23
100
  - lib/l2meter/configuration.rb
24
101
  - lib/l2meter/emitter.rb
25
- - lib/l2meter/null_object.rb
26
- - lib/l2meter/thread_safe.rb
102
+ - lib/l2meter/null_output.rb
27
103
  - lib/l2meter/version.rb
28
- homepage: https://github.com/rwz/l2meter
104
+ homepage: https://github.com/heroku/l2meter
29
105
  licenses:
30
106
  - MIT
31
- metadata: {}
107
+ metadata:
108
+ homepage_uri: https://github.com/heroku/l2meter
109
+ source_code_uri: https://github.com/heroku/l2meter
110
+ bug_tracker_uri: https://github.com/heroku/l2meter/issues
111
+ changelog_uri: https://github.com/heroku/l2meter/blob/main/CHANGELOG.md
32
112
  post_install_message:
33
113
  rdoc_options: []
34
114
  require_paths:
@@ -44,8 +124,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
44
124
  - !ruby/object:Gem::Version
45
125
  version: '0'
46
126
  requirements: []
47
- rubyforge_project:
48
- rubygems_version: 2.6.11
127
+ rubygems_version: 3.1.4
49
128
  signing_key:
50
129
  specification_version: 4
51
130
  summary: L2met friendly log formatter
@@ -1,11 +0,0 @@
1
- module L2meter
2
- class NullObject
3
- Emitter.instance_methods(false).each do |method_name|
4
- define_method method_name do |*, &block|
5
- block && block.call
6
- end
7
- end
8
-
9
- def print(*); end
10
- end
11
- end
@@ -1,54 +0,0 @@
1
- require "forwardable"
2
-
3
- module L2meter
4
- # This class is a wrapper around Emitter that makes sure that we have a
5
- # completely separate clone of Emitter per thread running. It doesn't truly
6
- # make Emitter thread-safe, it makes sure that you don't access the same
7
- # instance of emitter from different threads.
8
- class ThreadSafe
9
- extend Forwardable
10
-
11
- def initialize(emitter)
12
- @emitter = emitter.freeze
13
- end
14
-
15
- def_delegators :receiver, *Emitter.instance_methods(false)
16
-
17
- def context(*args, &block)
18
- value = current_emitter.context(*args, &block)
19
- Emitter === value ? clone_with_emitter(value) : value
20
- end
21
-
22
- def disable!
23
- @disabled = true
24
- end
25
-
26
- protected
27
-
28
- attr_writer :emitter
29
-
30
- private
31
-
32
- attr_reader :emitter
33
-
34
- def clone_with_emitter(emitter)
35
- self.class.new(emitter).tap { |ts| ts.disable! if @disabled }
36
- end
37
-
38
- def receiver
39
- @disabled ? null_emitter : current_emitter
40
- end
41
-
42
- def current_emitter
43
- Thread.current[thread_key] ||= emitter.clone
44
- end
45
-
46
- def null_emitter
47
- @null_emitter ||= NullObject.new
48
- end
49
-
50
- def thread_key
51
- @thread_key ||= "_l2meter_emitter_#{emitter.object_id}".freeze
52
- end
53
- end
54
- end