chalk-log 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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