chalk-log 0.1.2

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.
@@ -0,0 +1,18 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ .tddium*
data/Gemfile ADDED
@@ -0,0 +1,8 @@
1
+ # Execute bundler hook if present
2
+ ['~/.', '/etc/'].any? do |file|
3
+ File.lstat(path = File.expand_path(file + 'bundle-gemfile-hook')) rescue next
4
+ eval(File.read(path), binding, path); break true
5
+ end || source('https://rubygems.org/')
6
+
7
+ gemspec
8
+ gem 'pry'
@@ -0,0 +1,6 @@
1
+ === 0.1.0 2014-05-25
2
+
3
+ * Started being more strict with arguments passed. In particular,
4
+ stopped accepting nil or boolean trailing arguments.
5
+ * Switched to using Chalk::Config, eliminating Chalk::Log::Config.
6
+ * You now disable Chalk::Log by setting either of `configatron.chalk.log.disabled || LSpace[:'chalk.log.disabled']`. `ENV["CHALK_NOLOG"]` and `LSpace[:logging_disabled] are now ignored.
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Stripe
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,142 @@
1
+ # Chalk::Log
2
+
3
+ `Chalk::Log` adds a logger object to any class, which can be used for
4
+ unstructured or semi-structured logging. Use it as follows:
5
+
6
+ ```ruby
7
+ class A
8
+ include Chalk::Log
9
+ end
10
+
11
+ A.log.info('hello', key: 'value')
12
+ #=> [2013-06-18 22:18:28.314756] [64682] hello: key="value"
13
+ ```
14
+
15
+ The output is both human-digestable and easily parsed by log indexing
16
+ systems such as [Splunk](http://www.splunk.com/) or
17
+ [Logstash](http://logstash.net/).
18
+
19
+ It can also pretty-print exceptions for you:
20
+
21
+ ```ruby
22
+ module A; include Chalk::Log; end
23
+ begin; raise "hi"; rescue => e; end
24
+ A.log.error('Something went wrong', e)
25
+ #=> Something went wrong: hi (RuntimeError)
26
+ # (irb):8:in `irb_binding'
27
+ # /Users/gdb/.rbenv/versions/1.9.3-p362/lib/ruby/1.9.1/irb/workspace.rb:80:in `eval# /Users/gdb/.rbenv/versions/1.9.3-p362/lib/ruby/1.9.1/irb/workspace.rb:80:in `evaluate'
28
+ # /Users/gdb/.rbenv/versions/1.9.3-p362/lib/ruby/1.9.1/irb/context.rb:254:in `evaluate'
29
+ # /Users/gdb/.rbenv/versions/1.9.3-p362/lib/ruby/1.9.1/irb.rb:159:in `block (2 levels) in eval_input'
30
+ # [...]
31
+ ```
32
+
33
+ The log methods accept a message and/or an exception and/or an info
34
+ hash (if multiple are passed, they must be provided in that
35
+ order). The log methods will never throw an exception, but will
36
+ instead print an log message indicating they had a fault.
37
+
38
+ ## Overview
39
+
40
+ Including `Chalk::Log` creates a `log` method as both a class an
41
+ instance method, which returns a class-specific logger.
42
+
43
+ By default, it tags loglines with auxiliary information: a
44
+ microsecond-granularity timestamp, the PID, and an action_id (which
45
+ should tie together all lines for a single logical action in your
46
+ system, such as a web request).
47
+
48
+ You can turn off tagging, or just turn off timestamping, through
49
+ appropriate configatron settings (see [config.yaml](/config.yaml)).
50
+
51
+ There are also two `LSpace` dynamic settings available:
52
+
53
+ - `LSpace[:action_id]`: Set the action_id dynamically for this action. (This is used automatically by things like `Chalk::Web` which have a well-defined action.)
54
+ - `LSpace[:'chalk.log.disabled']`: Disable all logging.
55
+
56
+ You can use `LSpace` settings as follows:
57
+
58
+ ```ruby
59
+ class A; include Chalk::Log; end
60
+ foo = A.new
61
+
62
+ LSpace.with(action_id: 'request-123') do
63
+ foo.log.info('Test')
64
+ #=> [2014-05-26 01:12:28.485822] [47325|request-123] Test
65
+ end
66
+ ```
67
+
68
+ ## Log methods
69
+
70
+ `Chalk::Log` provides five log levels:
71
+
72
+ debug, info, warn, error, fatal
73
+
74
+ ## Inheritance
75
+
76
+ `Chalk::Log` makes a heroic effort to ensure that inclusion chaining
77
+ works, so you can do things like:
78
+
79
+ ```ruby
80
+ module A
81
+ include Chalk::Log
82
+ end
83
+
84
+ module B
85
+ include A
86
+ end
87
+
88
+ class C
89
+ include B
90
+ end
91
+ ```
92
+
93
+ and still have `C.log` and `C.new.log` work. (Normally you'd expect
94
+ for the class-method version to be left behind.)
95
+
96
+ ## Best practices
97
+
98
+ - You should never use string interpolation in your log
99
+ message. Instead, always use the structured logging keys. So for
100
+ example:
101
+
102
+ ```ruby
103
+ # Bad
104
+ log.info("Just printed #{lines.length} lines")
105
+ # Good
106
+ log.info("Printed", lines: lines.length)
107
+ ```
108
+
109
+ - Don't end messages with a punctuation -- `Chalk::Log` will
110
+ automatically add a colon if an info hash is provided; if not, it's
111
+ fine to just end without trailing punctutaion. Case in point
112
+
113
+ - In most projects, you'll find most of your classes start including
114
+ `Chalk::Log` -- it's pretty cheap to add it, and it's quite
115
+ lightweight to use. (In contrast, there's no good way to autoinclude
116
+ it, since that would likely break many classes which aren't
117
+ expecting a magical `log` method to appear.)
118
+
119
+ ## Limitations
120
+
121
+ `Chalk::Log` is not very configurable. Our usage at Stripe tends to be
122
+ fairly opinionated, so there hasn't been much demand for increased
123
+ configurability. We would be open to making it less rigid,
124
+ however. (In any case, under the hood `Chalk::Log` is just using the
125
+ `logging` gem, so if the need arises it wouldn't be hard to acquire
126
+ the full flexibility of `logging`.)
127
+
128
+ # Contributors
129
+
130
+ - Greg Brockman
131
+ - Andreas Fuchs
132
+ - Andy Brody
133
+ - Anurag Goel
134
+ - Evan Broder
135
+ - Nelson Elhage
136
+ - Brian Krausz
137
+ - Christian Anderson
138
+ - Jeff Balogh
139
+ - Jeremy Hoon
140
+ - Julia Evans
141
+ - Russell Davis
142
+ - Steven Noble
@@ -0,0 +1,17 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'bundler/setup'
3
+ require 'chalk-rake/gem_tasks'
4
+ require 'rake/testtask'
5
+
6
+ task :default do
7
+ sh 'rake -T'
8
+ end
9
+
10
+ Rake::TestTask.new do |t|
11
+ t.libs = ["lib"]
12
+ # t.warning = true
13
+ t.verbose = true
14
+ t.test_files = FileList['test/**/*.rb'].reject do |file|
15
+ file.end_with?('_lib.rb') || file.include?('/_lib/')
16
+ end
17
+ end
@@ -0,0 +1,26 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'chalk-log/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = 'chalk-log'
8
+ gem.version = Chalk::Log::VERSION
9
+ gem.authors = ['Stripe']
10
+ gem.email = ['oss@stripe.com']
11
+ gem.description = %q{Extends classes with a `log` method}
12
+ gem.summary = %q{Chalk::Log makes any class loggable. It provides a logger that can be used for both structured and unstructured log.}
13
+ gem.homepage = 'https://github.com/stripe/chalk-log'
14
+
15
+ gem.files = `git ls-files`.split($/)
16
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
17
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
18
+ gem.require_paths = ['lib']
19
+ gem.add_dependency 'chalk-config'
20
+ gem.add_dependency 'logging'
21
+ gem.add_dependency 'lspace'
22
+ gem.add_development_dependency 'rake'
23
+ gem.add_development_dependency 'minitest', '~> 3.2.0'
24
+ gem.add_development_dependency 'mocha'
25
+ gem.add_development_dependency 'chalk-rake'
26
+ end
@@ -0,0 +1,22 @@
1
+ chalk:
2
+ log:
3
+ # The default log level
4
+ default_level: 'INFO'
5
+
6
+ # Whether to enable tagging (with timestamp, PID, action_id).
7
+ tagging: true
8
+
9
+ # Whether to enable tagging with PID (can be subsumed by
10
+ # `tagging: false`)
11
+ pid: true
12
+
13
+ # Whether to enable tagging with timestamp (can be subsumed by
14
+ # `tagging: false`). This option is set automatically in
15
+ # lib/chalk-log.rb.
16
+ timestamp: null
17
+
18
+ # Whether to show logs at all
19
+ disabled: false
20
+
21
+ # Whether to remove gem lines from backtraces
22
+ compress_backtraces: false
@@ -0,0 +1,137 @@
1
+ require 'logging'
2
+ require 'lspace'
3
+ require 'set'
4
+
5
+ require 'chalk-config'
6
+ require 'chalk-log/version'
7
+
8
+ # Include `Chalk::Log` in a class or module to make that class (and
9
+ # all subclasses / includees / extendees) loggable. This creates a
10
+ # class and instance `log` method which you can call from within your
11
+ # loggable class.
12
+ #
13
+ # Loggers are per-class and can be manipulated as you'd expect:
14
+ #
15
+ # ```ruby
16
+ # class A
17
+ # include Chalk::Log
18
+ #
19
+ # log.level = 'DEBUG'
20
+ # log.debug('Now you see me!')
21
+ # log.level = 'INFO'
22
+ # log.debug('Now you do not!')
23
+ # end
24
+ # ```
25
+ #
26
+ # You shouldn't need to directly access any of the methods on
27
+ # `Chalk::Log` itself.
28
+ module Chalk::Log
29
+ require 'chalk-log/errors'
30
+ require 'chalk-log/logger'
31
+ require 'chalk-log/layout'
32
+ require 'chalk-log/utils'
33
+
34
+ # The set of available log methods. (Changing these is not currently
35
+ # a supported interface, though if the need arises it'd be easy to
36
+ # add.)
37
+ LEVELS = [:debug, :info, :warn, :error, :fatal].freeze
38
+
39
+ @included = Set.new
40
+
41
+ # Method which goes through heroic efforts to ensure that the whole
42
+ # inclusion hierarchy has their `log` accessors.
43
+ def self.included(other)
44
+ if other == Object
45
+ raise "You have attempted to `include Chalk::Log` onto Object. This is disallowed, since otherwise it might shadow any `log` method on classes that weren't expecting it (including, for example, `configatron.chalk.log`)."
46
+ end
47
+
48
+ # Already been through this ordeal; no need to repeat it. (There
49
+ # shouldn't be any semantic harm to doing so, just a potential
50
+ # performance hit.)
51
+ return if @included.include?(other)
52
+ @included << other
53
+
54
+ # Make sure to define the .log class method
55
+ other.extend(ClassMethods)
56
+
57
+ # If it's a module, we need to make sure both inclusion/extension
58
+ # result in virally carrying Chalk::Log inclusion downstream.
59
+ if other.instance_of?(Module)
60
+ other.class_eval do
61
+ included = method(:included)
62
+ extended = method(:extended)
63
+
64
+ define_singleton_method(:included) do |other|
65
+ other.send(:include, Chalk::Log)
66
+ included.call(other)
67
+ end
68
+
69
+ define_singleton_method(:extended) do |other|
70
+ other.send(:include, Chalk::Log)
71
+ extended.call(other)
72
+ end
73
+ end
74
+ end
75
+ end
76
+
77
+ # Public-facing initialization method for all `Chalk::Log`
78
+ # state. Unlike most other Chalk initializers, this will be
79
+ # automatically run (invoked on first logger instantiation). It is
80
+ # idempotent.
81
+ def self.init
82
+ return if @init
83
+ @init = true
84
+
85
+ # Load relevant configatron stuff
86
+ Chalk::Config.register(File.expand_path('../../config.yaml', __FILE__),
87
+ raw: true)
88
+
89
+ # The assumption is you'll pipe your logs through something like
90
+ # [Unilog](https://github.com/stripe/unilog) in production, which
91
+ # does its own timestamping.
92
+ Chalk::Config.register_raw(chalk: {log: {timestamp: STDERR.tty?}})
93
+
94
+ ::Logging.init(*LEVELS)
95
+ ::Logging.logger.root.add_appenders(
96
+ ::Logging.appenders.stderr(layout: layout)
97
+ )
98
+
99
+ Chalk::Log::Logger.init
100
+ end
101
+
102
+ # The default layout to use for the root `Logging::Logger`.
103
+ def self.layout
104
+ @layout ||= Chalk::Log::Layout.new
105
+ end
106
+
107
+ # Home of the backend `log` method people call; included *and*
108
+ # extended everywhere that includes Chalk::Log.
109
+ module ClassMethods
110
+ # The backend `log` method exposed to everyone. (In practice, the
111
+ # method people call directly is one wrapper above this.)
112
+ #
113
+ # Sets a `@__chalk_log` variable to hold the logger instance.
114
+ def log
115
+ @__chalk_log ||= Chalk::Log::Logger.new(self.name)
116
+ end
117
+ end
118
+
119
+ # Make the `log` method inheritable.
120
+ include ClassMethods
121
+
122
+ # The technique here is a bit tricky. The same `log` implementation
123
+ # defined on any class needs to be callable by either an instance or
124
+ # class. (See the "correctly make the end class loggable when it has
125
+ # already included loggable" test for why. In particular, someone
126
+ # may have already included me, and then clobbered the class
127
+ # implementations by extending me.) Hence we do this "defer to
128
+ # class, unless I am a class" logic.
129
+ log = instance_method(:log)
130
+ define_method(:log) do
131
+ if self.kind_of?(Class)
132
+ log.bind(self).call
133
+ else
134
+ self.class.log
135
+ end
136
+ end
137
+ end
@@ -0,0 +1,7 @@
1
+ module Chalk::Log
2
+ # Base error class
3
+ class Error < StandardError; end
4
+ # Thrown when you call a layout with the wrong arguments. (It gets
5
+ # swallowed and printed by the fault handling in layout.rb, though.)
6
+ class InvalidArguments < Error; end
7
+ end
@@ -0,0 +1,192 @@
1
+ require 'json'
2
+ require 'set'
3
+ require 'time'
4
+
5
+ # The layout backend for the Logging::Logger.
6
+ #
7
+ # Accepts a message and/or an exception and/or an info
8
+ # hash (if multiple are passed, they must be provided in that
9
+ # order)
10
+ class Chalk::Log::Layout < ::Logging::Layout
11
+ # Formats an event, and makes a heroic effort to tell you if
12
+ # something went wrong. (Logging will otherwise silently swallow any
13
+ # exceptions that get thrown.)
14
+ #
15
+ # @param event provided by the Logging::Logger
16
+ def format(event)
17
+ begin
18
+ begin
19
+ begin
20
+ do_format(event)
21
+ rescue StandardError => e
22
+ # Single fault!
23
+ error!('[Chalk::Log fault: Could not format message] ', e)
24
+ end
25
+ rescue StandardError => e
26
+ # Double fault!
27
+ "[Chalk::Log fault: Double fault while formatting message. This means we couldn't even report the error we got while formatting.] #{e.message}\n"
28
+ end
29
+ rescue StandardError => e
30
+ # Triple fault!
31
+ "[Chalk::Log fault: Triple fault while formatting message. This means we couldn't even report the error we got while reporting the original error.]\n"
32
+ end
33
+ end
34
+
35
+ # Formats a hash for logging. This is provided for (rare) use outside of log
36
+ # methods; you can pass a hash directly to log methods and this formatting
37
+ # will automatically be applied.
38
+ #
39
+ # @param hash [Hash] The hash to be formatted
40
+ def format_hash(hash)
41
+ hash.map {|k, v| display(k, v)}.join(' ')
42
+ end
43
+
44
+ private
45
+
46
+ def do_format(event)
47
+ data = event.data
48
+ time = event.time
49
+ level = event.level
50
+
51
+ # Data provided by blocks may not be arrays yet
52
+ data = [data] unless data.kind_of?(Array)
53
+ info = data.pop if data.last.kind_of?(Hash)
54
+ error = data.pop if data.last.kind_of?(Exception)
55
+ message = data.pop if data.last.kind_of?(String)
56
+
57
+ if data.length > 0
58
+ raise Chalk::Log::InvalidArguments.new("Invalid leftover arguments: #{data.inspect}")
59
+ end
60
+
61
+ id = action_id
62
+ pid = Process.pid
63
+
64
+ pretty_print(
65
+ time: timestamp_prefix(time),
66
+ level: Chalk::Log::LEVELS[level],
67
+ action_id: id,
68
+ message: message,
69
+ error: error,
70
+ info: (info && info.merge(contextual_info || {})) || contextual_info,
71
+ pid: pid
72
+ )
73
+ end
74
+
75
+ def pretty_print(spec)
76
+ message = build_message(spec[:message], spec[:info], spec[:error])
77
+ message = tag(message, spec[:time], spec[:pid], spec[:action_id])
78
+ message
79
+ end
80
+
81
+ def build_message(message, info, error)
82
+ # Make sure we're not mutating the message that was passed in
83
+ if message
84
+ message = message.dup
85
+ end
86
+
87
+ if message && (info || error)
88
+ message << ':'
89
+ end
90
+
91
+ if info
92
+ message << ' ' if message
93
+ message ||= ''
94
+ info!(message, info)
95
+ end
96
+
97
+ if error
98
+ message << ' ' if message
99
+ message ||= ''
100
+ error!(message, error)
101
+ end
102
+
103
+ message ||= ''
104
+ message << "\n"
105
+ message
106
+ end
107
+
108
+ # Displaying info hash
109
+
110
+ def info!(message, info)
111
+ message << format_hash(info.merge(contextual_info || {}))
112
+ end
113
+
114
+ def display(key, value)
115
+ begin
116
+ value = json(value)
117
+ rescue StandardError
118
+ value = "#{value.inspect} [JSON-FAILED]"
119
+ end
120
+
121
+ # Non-numeric simple strings don't need quotes.
122
+ if value =~ /\A"\w*[A-Za-z]\w*"\z/ &&
123
+ !['"true"', '"false"', '"null"'].include?(value)
124
+ value = value[1...-1]
125
+ end
126
+
127
+ "#{key}=#{value}"
128
+ end
129
+
130
+ # Displaying backtraces
131
+
132
+ def error!(message, error)
133
+ backtrace = error.backtrace || ['[no backtrace]']
134
+ message << display(:error_class, error.class.to_s) << " "
135
+ message << display(:error, error.to_s)
136
+ message << "\n"
137
+ message << Chalk::Log::Utils.format_backtrace(backtrace)
138
+ message << "\n"
139
+ message
140
+ end
141
+
142
+ def json(value)
143
+ # Use an Array (and trim later) because Ruby's JSON generator
144
+ # requires an array or object.
145
+ wrapped = [value]
146
+
147
+ # We may alias the raw JSON generation method. We don't care about
148
+ # emiting raw HTML tags heres, so no need to use the safe
149
+ # generation method.
150
+ if JSON.respond_to?(:unsafe_generate)
151
+ dumped = JSON.unsafe_generate(wrapped)
152
+ else
153
+ dumped = JSON.generate(wrapped)
154
+ end
155
+
156
+ dumped[1...-1] # strip off the brackets we added while array-ifying
157
+ end
158
+
159
+ def action_id
160
+ LSpace[:action_id]
161
+ end
162
+
163
+ def contextual_info
164
+ LSpace[:'chalk.log.contextual_info']
165
+ end
166
+
167
+ def tag(message, time, pid, action_id)
168
+ return message unless configatron.chalk.log.tagging
169
+
170
+ metadata = []
171
+ metadata << pid if configatron.chalk.log.pid
172
+ metadata << action_id if action_id
173
+ prefix = "[#{metadata.join('|')}] " if metadata.length > 0
174
+
175
+ if configatron.chalk.log.timestamp
176
+ prefix = "[#{time}] #{prefix}"
177
+ end
178
+
179
+ out = ''
180
+ message.split("\n").each do |line|
181
+ out << prefix << line << "\n"
182
+ end
183
+
184
+ out
185
+ end
186
+
187
+ def timestamp_prefix(now)
188
+ now_fmt = now.strftime("%Y-%m-%d %H:%M:%S")
189
+ ms_fmt = sprintf("%06d", now.usec)
190
+ "#{now_fmt}.#{ms_fmt}"
191
+ end
192
+ end