├── rubocop ├── config.yml ├── checks.rb ├── .rubocop.yml └── clevel_constant.rb ├── lib ├── chalk-log │ ├── version.rb │ ├── errors.rb │ ├── logger.rb │ ├── utils.rb │ └── layout.rb └── chalk-log.rb ├── .travis.yml ├── test ├── _lib │ ├── fake.rb │ └── fake │ │ └── event.rb ├── unit │ ├── _lib.rb │ ├── chalk-log │ │ └── utils.rb │ └── rubocop │ │ ├── clevel_constant.rb │ │ └── helpers.rb ├── functional │ ├── _lib.rb │ ├── log.rb │ └── formatting.rb └── _lib.rb ├── .gitignore ├── Gemfile ├── History.txt ├── Rakefile ├── config.yaml ├── LICENSE.txt ├── chalk-log.gemspec └── README.md /rubocop/config.yml: -------------------------------------------------------------------------------- 1 | require: ./checks 2 | -------------------------------------------------------------------------------- /rubocop/checks.rb: -------------------------------------------------------------------------------- 1 | require_relative './clevel_constant' 2 | -------------------------------------------------------------------------------- /lib/chalk-log/version.rb: -------------------------------------------------------------------------------- 1 | module Chalk 2 | module Log 3 | VERSION = '0.2.5' 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 2.1.10 4 | - 2.2.6 5 | - 2.3.3 6 | - 2.4.0 7 | script: rake test 8 | -------------------------------------------------------------------------------- /rubocop/.rubocop.yml: -------------------------------------------------------------------------------- 1 | inherit_gem: 2 | stripe-rubocop: .rubocop.yml 3 | 4 | inherit_from: 5 | - rubocop/config.yml 6 | -------------------------------------------------------------------------------- /test/_lib/fake.rb: -------------------------------------------------------------------------------- 1 | module Critic::Fake 2 | Dir[File.expand_path('../fake/*.rb', __FILE__)].each do |file| 3 | require_relative(file) 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /test/unit/_lib.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../../_lib', __FILE__) 2 | 3 | module Critic::Unit 4 | module Stubs 5 | end 6 | 7 | class Test < Critic::Test 8 | include Stubs 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /test/functional/_lib.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../../_lib', __FILE__) 2 | 3 | module Critic::Functional 4 | module Stubs 5 | end 6 | 7 | class Test < Critic::Test 8 | include Stubs 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 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* 19 | -------------------------------------------------------------------------------- /test/_lib/fake/event.rb: -------------------------------------------------------------------------------- 1 | class Critic::Fake::Event 2 | attr_reader :data, :time, :level 3 | 4 | def initialize(opts) 5 | @data = opts.fetch(:data) 6 | @time = opts.fetch(:time, Time.new(1979,4,9)) 7 | @level = opts.fetch(:level, 1) 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/chalk-log/errors.rb: -------------------------------------------------------------------------------- 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 8 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 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 'rubocop', '~> 0.47.1' 9 | gem 'pry' 10 | -------------------------------------------------------------------------------- /test/_lib.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'bundler/setup' 3 | 4 | require 'minitest/autorun' 5 | require 'minitest/spec' 6 | require 'mocha/setup' 7 | 8 | module Critic 9 | require_relative '_lib/fake' 10 | 11 | class Test < ::MiniTest::Spec 12 | def setup 13 | # Put any stubs here that you want to apply globally 14 | super 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /History.txt: -------------------------------------------------------------------------------- 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. 7 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 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 18 | -------------------------------------------------------------------------------- /test/unit/chalk-log/utils.rb: -------------------------------------------------------------------------------- 1 | require_relative '../_lib' 2 | require 'chalk-log' 3 | 4 | class Critic::Unit::Utils < Critic::Unit::Test 5 | describe '.explode_nested_hash' do 6 | it 'explodes nested keys' do 7 | hash = {foo: {bar: {baz: 'zom'}, zero: 'hello'}, hi: 'there'} 8 | exploded = Chalk::Log::Utils.explode_nested_hash(hash) 9 | assert_equal({"foo_bar_baz"=>"zom", "foo_zero"=>"hello", "hi"=>"there"}, 10 | exploded) 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /config.yaml: -------------------------------------------------------------------------------- 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 23 | 24 | # Whether to display backtraces at all 25 | display_backtraces: true 26 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 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. 23 | -------------------------------------------------------------------------------- /chalk-log.gemspec: -------------------------------------------------------------------------------- 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.required_ruby_version = '>= 1.9.3' 20 | gem.add_dependency 'chalk-config' 21 | gem.add_dependency 'logging' 22 | gem.add_dependency 'lspace' 23 | gem.add_development_dependency 'rake' 24 | gem.add_development_dependency 'minitest', '~> 5.5' 25 | gem.add_development_dependency 'mocha' 26 | gem.add_development_dependency 'chalk-rake' 27 | end 28 | -------------------------------------------------------------------------------- /test/unit/rubocop/clevel_constant.rb: -------------------------------------------------------------------------------- 1 | require_relative '../_lib' 2 | require_relative '../../../rubocop/clevel_constant.rb' 3 | require_relative './helpers.rb' 4 | require 'rubocop' 5 | 6 | 7 | module Critic::Unit 8 | class Critic::Unit::Test::CLevelConstantTest < Critic::Unit::Test 9 | include StripeRuboCop::Helpers::MinitestHelper 10 | 11 | def cop_classes 12 | [PrisonGuard::CLevelConstant] 13 | end 14 | 15 | def self.bad_log 16 | "log.info('bad log using string literal', clevel: 'invalid')" 17 | end 18 | 19 | def self.good_log 20 | "log.warn('good log using constant', clevel: Chalk::Log::CLevels::Sheddable)" 21 | end 22 | 23 | def self.block_with_contents(contents) 24 | "things.map do |x|\ 25 | #{contents}\ 26 | end" 27 | end 28 | 29 | good_cop('ignores empty strings', '') 30 | good_cop('ignores methods on non-log implicit parameters', 'notalog.info') 31 | good_cop('ignores methods on non-log implicit parameters', 'notalog.info("not a log line")') 32 | 33 | good_cop('ignores logs without a hash provided', 'log.info("no hash")') 34 | good_cop('ignores blocks', 'log.info("no hash")') 35 | 36 | good_cop('allows an info with constant', good_log) 37 | good_cop('allows an info with constant in multikey hash', 'log.warn("using constant", merchant: "foo", clevel: Chalk::Log::CLevels::Sheddable)') 38 | 39 | 40 | bad_cop('rejects an info with string literal', bad_log) 41 | bad_cop('rejects an info with an invalid clevel', 'log.info("something", clevel: Chalk::Log::CLevels::Sleddable)') 42 | bad_cop('catches static calls', 'Opus::Log.info("something", clevel: Chalk::Log::CLevels::Sleddable)') 43 | 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /rubocop/clevel_constant.rb: -------------------------------------------------------------------------------- 1 | require 'rubocop' 2 | 3 | module PrisonGuard 4 | # Ensure clevels are set using the Chalk::Log constants 5 | class CLevelConstant < RuboCop::Cop::Cop 6 | 7 | LOG_METHODS = %w{debug info warn error} 8 | 9 | VALID_CLEVELS_SEXPR = [ 10 | s(:const, 11 | s(:const, 12 | s(:const, 13 | s(:const, nil, :Chalk), :Log), :CLevels), :Sheddable), 14 | 15 | s(:const, 16 | s(:const, 17 | s(:const, 18 | s(:const, nil, :Chalk), :Log), :CLevels), :SheddablePlus), 19 | 20 | s(:const, 21 | s(:const, 22 | s(:const, 23 | s(:const, nil, :Chalk), :Log), :CLevels), :Critical), 24 | 25 | s(:const, 26 | s(:const, 27 | s(:const, 28 | s(:const, nil, :Chalk), :Log), :CLevels), :CriticalPlus), 29 | ] 30 | 31 | def investigate(processed_source) 32 | @file_path = processed_source.buffer.name 33 | @skip_file = @file_path.include?('/test/') 34 | end 35 | 36 | def on_send(node) 37 | return if @skip_file 38 | receiver, method_name, _args, hashargs = *node 39 | 40 | return unless receiver 41 | return unless receiver.children 42 | 43 | return if !LOG_METHODS.include?(method_name.to_s) 44 | return unless hashargs 45 | return if hashargs.type != :hash 46 | 47 | hashargs.children.map do |pair| 48 | next if pair.children && pair.children[0].to_a[0] != :clevel 49 | 50 | if pair.children[1].type != :const 51 | add_offense(node, :expression, "Non-constant clevel specified: #{pair.children[1]}. Use Chalk::Log::Clevels::Sheddable") 52 | end 53 | 54 | if !VALID_CLEVELS_SEXPR.include?(pair.children[1]) 55 | add_offense(node, :expression, "Invalid clevel: #{pair.children[1]}") 56 | end 57 | end 58 | end 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /test/unit/rubocop/helpers.rb: -------------------------------------------------------------------------------- 1 | # this code from stripe-rubocop/lib/stripe-rubocop/helpers/minitest_helper.rb 2 | # necessary to write repo-specific rubocop tests 3 | require 'rubocop' 4 | require 'minitest/autorun' 5 | require 'minitest/spec' 6 | 7 | module StripeRuboCop 8 | module Helpers 9 | module MinitestHelper 10 | module ClassMethods 11 | def good_cop(name, code) 12 | it "accepts #{name}" do 13 | off = investigate_string(code) 14 | assert(off.empty?, "Expected no offenses, got: #{off.inspect}") 15 | end 16 | end 17 | 18 | def bad_cop(name, code) 19 | it "rejects #{name}" do 20 | assert(!investigate_string(code).empty?, "expected offenses on: #{code}") 21 | end 22 | end 23 | 24 | def corrects(name, from, to) 25 | it "corrects #{name}" do 26 | assert_equal(to, correct_string(from)) 27 | end 28 | end 29 | end 30 | 31 | def self.included(other) 32 | other.extend(ClassMethods) 33 | end 34 | 35 | def cop_classes 36 | raise NotImplementedError 37 | end 38 | 39 | def source_path 40 | nil 41 | end 42 | 43 | def create_cops 44 | cfg = RuboCop::Config.new 45 | cop_classes.map {|kls| kls.new(cfg, debug: true, auto_correct: true)} 46 | end 47 | 48 | def commissioner(cops) 49 | RuboCop::Cop::Commissioner.new(cops, [], raise_error: true) 50 | end 51 | 52 | def investigate_string(str) 53 | src = RuboCop::ProcessedSource.new(str, RUBY_VERSION.to_f, source_path) 54 | commissioner(create_cops).investigate(src) 55 | end 56 | 57 | def correct_string(str) 58 | cops = create_cops 59 | 60 | src = RuboCop::ProcessedSource.new(str, RUBY_VERSION.to_f, source_path) 61 | corrector = RuboCop::Cop::Corrector.new(src.buffer) 62 | 63 | # Populates corrections 64 | commissioner(cops).investigate(src) 65 | 66 | cops.each do |cop| 67 | corrector.corrections.concat(cop.corrections) 68 | end 69 | 70 | corrector.rewrite 71 | end 72 | end 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /lib/chalk-log/logger.rb: -------------------------------------------------------------------------------- 1 | # Thin wrapper over Logging::Logger. This is the per-class object 2 | # instantiated by the `log` method. 3 | class Chalk::Log::Logger 4 | attr_reader :backend 5 | 6 | ROOT_LOGGER_NAME = "CHALK_LOG_ROOT".freeze 7 | private_constant :ROOT_LOGGER_NAME 8 | 9 | # Initialization of the logger backend. It does the actual creation 10 | # of the various logger methods. Will be called automatically upon 11 | # your first `log` method call. 12 | def self.init 13 | return if @initialized 14 | @initialized = true 15 | 16 | Chalk::Log::LEVELS.each do |level| 17 | define_method(level) do |*data, &blk| 18 | return if logging_disabled? 19 | @backend.send(level, data, &blk) 20 | end 21 | end 22 | end 23 | 24 | # The level this logger is set to. 25 | def level 26 | @backend.level 27 | end 28 | 29 | # Set the maximum log level. 30 | # 31 | # @param level [Fixnum|String|Symbol] A valid Logging::Logger level, e.g. :debug, 0, 'DEBUG', etc. 32 | def level=(level) 33 | @backend.level = level 34 | end 35 | 36 | # Create a new logger, and auto-initialize everything. 37 | def initialize(name) 38 | # It's generally a bad pattern to auto-init, but we want 39 | # Chalk::Log to be usable anytime during the boot process, which 40 | # requires being a little bit less explicit than we usually like. 41 | Chalk::Log.init 42 | 43 | # The Logging library parses the logger name to determine the correct parent 44 | name = Chalk::Log._root_backend.name + ::Logging::Repository::PATH_DELIMITER + (name || 'ANONYMOUS') 45 | @backend = ::Logging::Logger.new(name) 46 | end 47 | 48 | # Check whether logging has been globally turned off, either through 49 | # configatron or LSpace. 50 | def logging_disabled? 51 | configatron.chalk.log.disabled || LSpace[:'chalk.log.disabled'] 52 | end 53 | 54 | def with_contextual_info(contextual_info={}, &blk) 55 | unless blk 56 | raise ArgumentError.new("Must pass a block to #{__method__}") 57 | end 58 | unless contextual_info.is_a?(Hash) 59 | raise TypeError.new( 60 | "contextual_info must be a Hash, but got #{contextual_info.class}" 61 | ) 62 | end 63 | existing_context = LSpace[:'chalk.log.contextual_info'] || {} 64 | LSpace.with( 65 | :'chalk.log.contextual_info' => contextual_info.merge(existing_context), 66 | &blk 67 | ) 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /lib/chalk-log/utils.rb: -------------------------------------------------------------------------------- 1 | module Chalk::Log::Utils 2 | # Nicely formats a backtrace: 3 | # 4 | # ```ruby 5 | # format_backtrace(['line1', 'line2']) 6 | # #=> line1 7 | # line2 8 | # ``` 9 | # 10 | # (Used internally when `Chalk::Log` is formatting exceptions.) 11 | # 12 | # TODO: add autotruncating of backtraces. 13 | def self.format_backtrace(backtrace) 14 | if configatron.chalk.log.compress_backtraces 15 | backtrace = compress_backtrace(backtrace) 16 | backtrace << '(Disable backtrace compression by setting configatron.chalk.log.compress_backtraces = false.)' 17 | end 18 | 19 | " " + backtrace.join("\n ") 20 | end 21 | 22 | # Explodes a nested hash to just have top-level keys. This is 23 | # generally useful if you have something that knows how to parse 24 | # kv-pairs. 25 | # 26 | # ```ruby 27 | # explode_nested_hash(foo: {bar: 'baz', bat: 'zom'}) 28 | # #=> {'foo_bar' => 'baz', 'foo_bat' => 'zom'} 29 | # ``` 30 | def self.explode_nested_hash(hash, prefix=nil) 31 | exploded = {} 32 | 33 | hash.each do |key, value| 34 | new_prefix = prefix ? "#{prefix}_#{key}" : key.to_s 35 | 36 | if value.is_a?(Hash) 37 | exploded.merge!(self.explode_nested_hash(value, new_prefix)) 38 | else 39 | exploded[new_prefix] = value 40 | end 41 | end 42 | 43 | exploded 44 | end 45 | 46 | # Compresses a backtrace, omitting gem lines (unless they appear 47 | # before any application lines). 48 | def self.compress_backtrace(backtrace) 49 | compressed = [] 50 | gemdir = Gem.dir 51 | 52 | hit_application = false 53 | # This isn't currently read by anything, but we could easily use 54 | # it to limit the number of leading gem lines. 55 | leading_lines = 0 56 | gemlines = 0 57 | backtrace.each do |line| 58 | if line.start_with?(gemdir) 59 | # If we're in a gem, always increment the counter. Record the 60 | # first three lines if we haven't seen any application lines 61 | # yet. 62 | if !hit_application 63 | compressed << line 64 | leading_lines += 1 65 | else 66 | gemlines += 1 67 | end 68 | elsif gemlines > 0 69 | # If we were in a gem and now are not, record the number of 70 | # lines skipped. 71 | compressed << "<#{gemlines} #{gemlines == 1 ? 'line' : 'lines'} omitted>" 72 | compressed << line 73 | hit_application = true 74 | gemlines = 0 75 | else 76 | # If we're in the application, always record the line. 77 | compressed << line 78 | hit_application = true 79 | end 80 | end 81 | 82 | compressed 83 | end 84 | end 85 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DEPRECATED: Stripe no longer support Chalk::Log 2 | 3 | # Chalk::Log 4 | 5 | `Chalk::Log` adds a logger object to any class, which can be used for 6 | unstructured or semi-structured logging. Use it as follows: 7 | 8 | ```ruby 9 | class A 10 | include Chalk::Log 11 | end 12 | 13 | A.log.info('hello', key: 'value') 14 | #=> [2013-06-18 22:18:28.314756] [64682] hello: key="value" 15 | ``` 16 | 17 | The output is both human-digestable and easily parsed by log indexing 18 | systems such as [Splunk](http://www.splunk.com/) or 19 | [Logstash](http://logstash.net/). 20 | 21 | It can also pretty-print exceptions for you: 22 | 23 | ```ruby 24 | module A 25 | include Chalk::Log 26 | end 27 | begin 28 | raise "hi" 29 | rescue => e 30 | end 31 | 32 | A.log.error('Something went wrong', e) 33 | #=> Something went wrong: hi (RuntimeError) 34 | # (irb):8:in `irb_binding' 35 | # /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' 36 | # /Users/gdb/.rbenv/versions/1.9.3-p362/lib/ruby/1.9.1/irb/context.rb:254:in `evaluate' 37 | # /Users/gdb/.rbenv/versions/1.9.3-p362/lib/ruby/1.9.1/irb.rb:159:in `block (2 levels) in eval_input' 38 | # [...] 39 | ``` 40 | 41 | The log methods accept a message and/or an exception and/or an info 42 | hash (if multiple are passed, they must be provided in that 43 | order). The log methods will never throw an exception, but will 44 | instead print an log message indicating they had a fault. 45 | 46 | ## Overview 47 | 48 | Including `Chalk::Log` creates a `log` method as both a class an 49 | instance method, which returns a class-specific logger. 50 | 51 | By default, it tags loglines with auxiliary information: a 52 | microsecond-granularity timestamp, the PID, and an action_id (which 53 | should tie together all lines for a single logical action in your 54 | system, such as a web request). 55 | 56 | You can turn off tagging, or just turn off timestamping, through 57 | appropriate configatron settings (see [config.yaml](/config.yaml)). 58 | 59 | There are also two `LSpace` dynamic settings available: 60 | 61 | - `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.) 62 | - `LSpace[:'chalk.log.disabled']`: Disable all logging. 63 | 64 | You can use `LSpace` settings as follows: 65 | 66 | ```ruby 67 | class A; include Chalk::Log; end 68 | foo = A.new 69 | 70 | LSpace.with(action_id: 'request-123') do 71 | foo.log.info('Test') 72 | #=> [2014-05-26 01:12:28.485822] [47325|request-123] Test 73 | end 74 | ``` 75 | 76 | ## Log methods 77 | 78 | `Chalk::Log` provides five log levels: 79 | 80 | debug, info, warn, error, fatal 81 | 82 | ## Inheritance 83 | 84 | `Chalk::Log` makes a heroic effort to ensure that inclusion chaining 85 | works, so you can do things like: 86 | 87 | ```ruby 88 | module A 89 | include Chalk::Log 90 | end 91 | 92 | module B 93 | include A 94 | end 95 | 96 | class C 97 | include B 98 | end 99 | ``` 100 | 101 | and still have `C.log` and `C.new.log` work. (Normally you'd expect 102 | for the class-method version to be left behind.) 103 | 104 | ## Best practices 105 | 106 | - You should never use string interpolation in your log 107 | message. Instead, always use the structured logging keys. So for 108 | example: 109 | 110 | ```ruby 111 | # Bad 112 | log.info("Just printed #{lines.length} lines") 113 | # Good 114 | log.info("Printed", lines: lines.length) 115 | ``` 116 | 117 | - Don't end messages with a punctuation -- `Chalk::Log` will 118 | automatically add a colon if an info hash is provided; if not, it's 119 | fine to just end without trailing punctutaion. Case in point 120 | 121 | - In most projects, you'll find most of your classes start including 122 | `Chalk::Log` -- it's pretty cheap to add it, and it's quite 123 | lightweight to use. (In contrast, there's no good way to autoinclude 124 | it, since that would likely break many classes which aren't 125 | expecting a magical `log` method to appear.) 126 | 127 | ## Limitations 128 | 129 | `Chalk::Log` is not very configurable. Our usage at Stripe tends to be 130 | fairly opinionated, so there hasn't been much demand for increased 131 | configurability. We would be open to making it less rigid, 132 | however. (In any case, under the hood `Chalk::Log` is just using the 133 | `logging` gem, so if the need arises it wouldn't be hard to acquire 134 | the full flexibility of `logging`.) 135 | 136 | # Contributors 137 | 138 | - Greg Brockman 139 | - Andreas Fuchs 140 | - Andy Brody 141 | - Anurag Goel 142 | - Evan Broder 143 | - Nelson Elhage 144 | - Brian Krausz 145 | - Christian Anderson 146 | - Jeff Balogh 147 | - Jeremy Hoon 148 | - Julia Evans 149 | - Russell Davis 150 | - Steven Noble 151 | -------------------------------------------------------------------------------- /lib/chalk-log.rb: -------------------------------------------------------------------------------- 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 | module CLevels 40 | Sheddable = :sheddable 41 | SheddablePlus = :sheddableplus 42 | Critical = :critical 43 | CriticalPlus = :criticalplus 44 | end 45 | 46 | @included = Set.new 47 | 48 | # Method which goes through heroic efforts to ensure that the whole 49 | # inclusion hierarchy has their `log` accessors. 50 | def self.included(other) 51 | if other == Object 52 | 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`)." 53 | end 54 | 55 | # Already been through this ordeal; no need to repeat it. (There 56 | # shouldn't be any semantic harm to doing so, just a potential 57 | # performance hit.) 58 | return if @included.include?(other) 59 | @included << other 60 | 61 | # Make sure to define the .log class method 62 | other.extend(ClassMethods) 63 | 64 | # If it's a module, we need to make sure both inclusion/extension 65 | # result in virally carrying Chalk::Log inclusion downstream. 66 | if other.instance_of?(Module) 67 | other.class_eval do 68 | included = method(:included) 69 | extended = method(:extended) 70 | 71 | define_singleton_method(:included) do |other| 72 | other.send(:include, Chalk::Log) 73 | included.call(other) 74 | end 75 | 76 | define_singleton_method(:extended) do |other| 77 | other.send(:include, Chalk::Log) 78 | extended.call(other) 79 | end 80 | end 81 | end 82 | end 83 | 84 | # Public-facing initialization method for all `Chalk::Log` 85 | # state. Unlike most other Chalk initializers, this will be 86 | # automatically run (invoked on first logger instantiation). It is 87 | # idempotent. 88 | def self.init 89 | return if @init 90 | @init = true 91 | 92 | # Load relevant configatron stuff 93 | Chalk::Config.register(File.expand_path('../../config.yaml', __FILE__), 94 | raw: true) 95 | 96 | # The assumption is you'll pipe your logs through something like 97 | # [Unilog](https://github.com/stripe/unilog) in production, which 98 | # does its own timestamping. 99 | Chalk::Config.register_raw(chalk: {log: {timestamp: STDERR.tty?}}) 100 | 101 | ::Logging.init(*LEVELS) 102 | ::Logging.logger.root.add_appenders( 103 | ::Logging.appenders.stderr(layout: layout) 104 | ) 105 | 106 | Chalk::Log::Logger.init 107 | end 108 | 109 | # The default layout to use for the root `Logging::Logger`. 110 | def self.layout 111 | @layout ||= Chalk::Log::Layout.new 112 | end 113 | 114 | # Adds a prefix to all logging within the current LSpace context. 115 | def self.with_message_prefix(prefix, &blk) 116 | LSpace.with(:'chalk.log.message_prefix' => prefix, &blk) 117 | end 118 | 119 | def self.message_prefix 120 | LSpace[:'chalk.log.message_prefix'] 121 | end 122 | 123 | def self.level=(lvl) 124 | _root_backend.level = lvl 125 | end 126 | 127 | def self.level 128 | _root_backend.level 129 | end 130 | 131 | # This should only be called from Chalk::Log::Logger 132 | def self._root_backend 133 | @root_backend ||= begin 134 | backend = ::Logging::Logger.new("CHALK_LOG_ROOT") 135 | if (level = configatron.chalk.log.default_level) 136 | backend.level = level 137 | end 138 | backend 139 | end 140 | end 141 | 142 | # Home of the backend `log` method people call; included *and* 143 | # extended everywhere that includes Chalk::Log. 144 | module ClassMethods 145 | # The backend `log` method exposed to everyone. (In practice, the 146 | # method people call directly is one wrapper above this.) 147 | # 148 | # Sets a `@__chalk_log` variable to hold the logger instance. 149 | def log 150 | @__chalk_log ||= Chalk::Log::Logger.new(self.name) 151 | end 152 | end 153 | 154 | # Make the `log` method inheritable. 155 | include ClassMethods 156 | 157 | # The technique here is a bit tricky. The same `log` implementation 158 | # defined on any class/module needs to be callable by either an instance or 159 | # the class/module itself. (See the "correctly make the end class loggable when it has 160 | # already included loggable" test for why. In particular, someone 161 | # may have already included me, and then clobbered the class/module 162 | # implementations by extending me.) Hence we do this "defer to 163 | # class, unless I am a class/module" logic. 164 | log = instance_method(:log) 165 | define_method(:log) do 166 | if self.kind_of?(Module) 167 | log.bind(self).call 168 | else 169 | self.class.log 170 | end 171 | end 172 | end 173 | -------------------------------------------------------------------------------- /lib/chalk-log/layout.rb: -------------------------------------------------------------------------------- 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 | data = data.dup # Make a private copy that we can mutate 54 | info = data.pop if data.last.kind_of?(Hash) 55 | error = data.pop if data.last.kind_of?(Exception) 56 | message = data.pop if data.last.kind_of?(String) 57 | 58 | if data.length > 0 59 | raise Chalk::Log::InvalidArguments.new("Invalid leftover arguments: #{data.inspect}") 60 | end 61 | 62 | pid = Process.pid 63 | 64 | pretty_print( 65 | time: timestamp_prefix(time), 66 | level: Chalk::Log::LEVELS[level], 67 | span: span.to_s, 68 | message: message, 69 | error: error, 70 | info: (info && (contextual_info || {}).merge(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[:span]) 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 Chalk::Log.message_prefix 92 | message ||= '' 93 | message.prepend(Chalk::Log.message_prefix) 94 | end 95 | 96 | if info 97 | message << ' ' if message 98 | message ||= '' 99 | message << format_hash(info) 100 | end 101 | 102 | if error 103 | message << ' ' if message 104 | message ||= '' 105 | error!(message, error) 106 | end 107 | 108 | message ||= '' 109 | message << "\n" 110 | message 111 | end 112 | 113 | def display(key, value) 114 | begin 115 | value = json(value) 116 | rescue StandardError 117 | value = "#{value.inspect} [JSON-FAILED]" 118 | end 119 | 120 | # Non-numeric simple strings don't need quotes. 121 | if value =~ /\A"\w*[A-Za-z]\w*"\z/ && 122 | !['"true"', '"false"', '"null"'].include?(value) 123 | value = value[1...-1] 124 | end 125 | 126 | "#{key}=#{value}" 127 | end 128 | 129 | # Displaying backtraces 130 | 131 | def error!(message, error) 132 | backtrace = error.backtrace || ['[no backtrace]'] 133 | message << display(:error_class, error.class.to_s) << " " 134 | message << display(:error, error.to_s) 135 | if configatron.chalk.log.display_backtraces 136 | message << "\n" 137 | message << Chalk::Log::Utils.format_backtrace(backtrace) 138 | message << "\n" 139 | end 140 | message 141 | end 142 | 143 | def json(value) 144 | # Use an Array (and trim later) because Ruby's JSON generator 145 | # requires an array or object. 146 | wrapped = [value] 147 | 148 | # We may alias the raw JSON generation method. We don't care about 149 | # emiting raw HTML tags heres, so no need to use the safe 150 | # generation method. 151 | if JSON.respond_to?(:unsafe_generate) 152 | dumped = JSON.unsafe_generate(wrapped) 153 | else 154 | dumped = JSON.generate(wrapped) 155 | end 156 | 157 | res = dumped[1...-1] # strip off the brackets we added while array-ifying 158 | 159 | # Bug 6566 in ruby 2.0 (but not 2.1) allows generate() to return an invalid 160 | # string when given invalid unicode input. Manually check for it. 161 | unless res.valid_encoding? 162 | raise ArgumentError.new("invalid byte sequence in UTF-8") 163 | end 164 | 165 | res 166 | end 167 | 168 | def contextual_info 169 | LSpace[:'chalk.log.contextual_info'] 170 | end 171 | 172 | def span 173 | LSpace[:span] || LSpace[:action_id] 174 | end 175 | 176 | def tag(message, time, pid, span) 177 | return message unless configatron.chalk.log.tagging 178 | 179 | metadata = [] 180 | metadata << pid if configatron.chalk.log.pid 181 | metadata << span if span.length > 0 182 | prefix = "[#{metadata.join('|')}] " if metadata.length > 0 183 | 184 | if configatron.chalk.log.timestamp 185 | prefix = "[#{time}] #{prefix}" 186 | end 187 | 188 | out = '' 189 | message.split("\n").each do |line| 190 | out << prefix << line << "\n" 191 | end 192 | 193 | out 194 | end 195 | 196 | def timestamp_prefix(now) 197 | now_fmt = now.strftime("%Y-%m-%d %H:%M:%S") 198 | ms_fmt = sprintf("%06d", now.usec) 199 | "#{now_fmt}.#{ms_fmt}" 200 | end 201 | end 202 | -------------------------------------------------------------------------------- /test/functional/log.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../_lib', __FILE__) 2 | 3 | require 'chalk-log' 4 | 5 | module Critic::Functional 6 | class LogTest < Test 7 | def self.add_appender 8 | unless @appender 9 | @appender = ::Logging.appenders.string_io( 10 | 'test-stringio', 11 | layout: Chalk::Log.layout 12 | ) 13 | ::Logging.logger.root.add_appenders(@appender) 14 | end 15 | @appender 16 | end 17 | 18 | before do 19 | Chalk::Log.init 20 | end 21 | 22 | describe 'when a class has included Log' do 23 | it 'instances are loggable' do 24 | class MyClass 25 | include Chalk::Log 26 | end 27 | 28 | MyClass.new.log.info("Hi!") 29 | end 30 | 31 | it 'the class is loggable' do 32 | class YourClass 33 | include Chalk::Log 34 | end 35 | 36 | YourClass.log.info("Hi!") 37 | end 38 | end 39 | 40 | describe 'including a loggable module into another' do 41 | describe 'the inclusions are straightline' do 42 | it 'make the includee loggable' do 43 | module LogTestA 44 | include Chalk::Log 45 | end 46 | 47 | module LogTestB 48 | include LogTestA 49 | end 50 | 51 | assert(LogTestB < Chalk::Log) 52 | assert(LogTestB.respond_to?(:log)) 53 | end 54 | 55 | it 'preserves any custom include logic prior to Log inclusion' do 56 | module CustomLogTestA 57 | def self.dict=(dict) 58 | @dict = dict 59 | end 60 | 61 | def self.dict 62 | @dict 63 | end 64 | 65 | def self.included(other) 66 | dict['included'] = true 67 | end 68 | 69 | include Chalk::Log 70 | end 71 | 72 | dict = {} 73 | CustomLogTestA.dict = dict 74 | 75 | module CustomLogTestB 76 | include CustomLogTestA 77 | end 78 | 79 | assert(CustomLogTestB < Chalk::Log) 80 | assert(CustomLogTestB.respond_to?(:log)) 81 | assert_equal(true, dict['included']) 82 | end 83 | 84 | # TODO: it'd be nice if this weren't true, but I'm not sure 85 | # how to get a hook when a method is overriden. 86 | it 'custom include logic after Log inclusion clobbers the default include logic' do 87 | module CustomLogTestC 88 | def self.dict=(dict) 89 | @dict = dict 90 | end 91 | 92 | def self.dict 93 | @dict 94 | end 95 | 96 | include Chalk::Log 97 | 98 | def self.included(other) 99 | dict['included'] = true 100 | end 101 | end 102 | 103 | dict = {} 104 | CustomLogTestC.dict = dict 105 | 106 | module CustomLogTestD 107 | include CustomLogTestC 108 | end 109 | 110 | assert(CustomLogTestD < Chalk::Log) 111 | assert(!CustomLogTestD.respond_to?(:log)) 112 | assert_equal(true, dict['included']) 113 | end 114 | end 115 | end 116 | 117 | describe 'extending a Log module into another' do 118 | describe 'the inclusions are straightline' do 119 | it 'make the extendee loggable' do 120 | module ExtendLogTestA 121 | include Chalk::Log 122 | def say_hi 123 | log.info('hello') 124 | end 125 | end 126 | 127 | module ExtendLogTestB 128 | extend ExtendLogTestA 129 | end 130 | 131 | assert(ExtendLogTestB < Chalk::Log) 132 | assert(ExtendLogTestB.respond_to?(:log)) 133 | assert(ExtendLogTestB.say_hi) 134 | end 135 | end 136 | end 137 | 138 | describe 'when a class is loggable' do 139 | class MyLog 140 | include Chalk::Log 141 | end 142 | 143 | it 'log.warn works' do 144 | msg = 'msg' 145 | # For some reason this isn't working: 146 | MyLog.log.backend.expects(:warn).once 147 | MyLog.log.warn(msg) 148 | end 149 | 150 | it 'log.info works' do 151 | msg = 'msg' 152 | MyLog.log.backend.expects(:info).once 153 | MyLog.log.info(msg) 154 | end 155 | 156 | it 'accepts blocks' do 157 | class LogTestE 158 | include Chalk::Log 159 | end 160 | LogTestE.log.level = "INFO" 161 | 162 | LogTestE.log.debug { assert(false, "DEBUG block called when at INFO level") } 163 | called = false 164 | LogTestE.log.info { called = true; "" } 165 | assert(called, "INFO block not called at INFO level") 166 | end 167 | 168 | it 'respects changes to the global log level by default' do 169 | begin 170 | root_lvl = Chalk::Log.level 171 | Chalk::Log.level = "WARN" 172 | 173 | MyLog.log.info { assert(false, "INFO block called at WARN level") } 174 | ensure 175 | Chalk::Log.level = root_lvl 176 | end 177 | end 178 | 179 | it 'ignores changes to the global log level when a local one is set' do 180 | begin 181 | root_lvl = Chalk::Log.level 182 | MyLog.log.level = "INFO" 183 | Chalk::Log.level = "WARN" 184 | 185 | called = false 186 | MyLog.log.info { called = true; "" } 187 | assert(called, "INFO block not called at INFO level") 188 | ensure 189 | Chalk::Log.level = root_lvl 190 | MyLog.log.level = nil 191 | end 192 | end 193 | end 194 | 195 | class TestLogInstanceMethods < Test 196 | include Chalk::Log 197 | 198 | before do 199 | TestLogInstanceMethods.log.level = 'INFO' 200 | Chalk::Log.init 201 | @appender = LogTest.add_appender 202 | end 203 | 204 | def assert_logged(expected_string, *args) 205 | assert_includes(@appender.sio.string, expected_string, *args) 206 | end 207 | 208 | it 'accepts blocks on instance methods' do 209 | called = false 210 | log.debug { assert(false, "DEBUG block called at INFO") } 211 | log.info { called = true; "" } 212 | assert(called, "INFO block not called at INFO level") 213 | end 214 | 215 | describe 'when contextual info is set' do 216 | 217 | it 'adds contextual information to `.info` log lines' do 218 | log.with_contextual_info(key: 'value') {log.info("message")} 219 | assert_logged("key=value") 220 | end 221 | 222 | it 'nests contexts' do 223 | log.with_contextual_info(top_key: "top_value") do 224 | log.info("top message") 225 | log.with_contextual_info(inner_key: "inner_value") do 226 | log.info("inner message") 227 | end 228 | end 229 | %w{top_key=top_value inner_key=inner_value}.each do |pair| 230 | assert_logged(pair) 231 | end 232 | end 233 | 234 | it 'prefers explicit information over the context' do 235 | log.with_contextual_info(omg: 'wtf') do 236 | log.info("message", omg: 'ponies') 237 | end 238 | assert_logged("omg=ponies") 239 | end 240 | 241 | it 'requires a block' do 242 | exn = assert_raises(ArgumentError) do 243 | log.with_contextual_info(i_am_not: "passing a block") 244 | end 245 | assert_includes(exn.message, "Must pass a block") 246 | end 247 | 248 | it 'requires its argument must be a hash' do 249 | exn = assert_raises(TypeError) do 250 | log.with_contextual_info('not a hash') {} 251 | end 252 | assert_includes(exn.message, 'must be a Hash') 253 | end 254 | end 255 | end 256 | 257 | describe 'when chaining includes and extends' do 258 | it 'correctly make the end class loggable' do 259 | module Base1 260 | include Chalk::Log 261 | end 262 | 263 | class Child1 264 | extend Base1 265 | end 266 | 267 | Child1.log.info("Hello!") 268 | assert(true) 269 | end 270 | 271 | it 'correctly make the end class loggable when chaining an include and extend' do 272 | module Base2 273 | include Chalk::Log 274 | end 275 | 276 | module Middle2 277 | extend Base2 278 | end 279 | 280 | class Child2 281 | include Middle2 282 | end 283 | 284 | Child2.log.info("Hello!") 285 | assert(true) 286 | end 287 | 288 | it 'correctly make the end class loggable when chaining an extend and an extend' do 289 | module Base3 290 | include Chalk::Log 291 | end 292 | 293 | module Middle3 294 | extend Base3 295 | end 296 | 297 | class Child3 298 | extend Middle3 299 | end 300 | 301 | Child3.log.info("Hello!") 302 | assert(true) 303 | end 304 | 305 | it 'correctly make the end class loggable when it has already included loggable' do 306 | module Base4 307 | include Chalk::Log 308 | end 309 | 310 | module Middle4 311 | extend Base4 312 | end 313 | 314 | class Child4 315 | include Chalk::Log 316 | extend Middle4 317 | end 318 | 319 | Child4.log.info("Hello!") 320 | assert(true) 321 | end 322 | end 323 | 324 | it 'correctly makes a module loggable' do 325 | module Base5 326 | include Chalk::Log 327 | end 328 | 329 | Base5.log.info("Hello!") 330 | assert(true) 331 | end 332 | end 333 | end 334 | -------------------------------------------------------------------------------- /test/functional/formatting.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../_lib', __FILE__) 2 | 3 | require 'chalk-log' 4 | 5 | module Critic::Functional 6 | class FormattingTest < Test 7 | def enable_timestamp 8 | configatron.unlock! do 9 | configatron.chalk.log.timestamp = true 10 | end 11 | end 12 | 13 | def disable_timestamp 14 | configatron.unlock! do 15 | configatron.chalk.log.timestamp = false 16 | end 17 | end 18 | 19 | def disable_pid 20 | configatron.unlock! do 21 | configatron.chalk.log.pid = false 22 | end 23 | end 24 | 25 | def disable_tagging 26 | configatron.unlock! do 27 | configatron.chalk.log.tagging = false 28 | end 29 | end 30 | 31 | def disable_backtraces 32 | configatron.unlock! do 33 | configatron.chalk.log.display_backtraces = false 34 | end 35 | end 36 | 37 | before do 38 | Chalk::Log.init 39 | Process.stubs(:pid).returns(9973) 40 | configatron.temp_start 41 | disable_timestamp 42 | end 43 | 44 | after do 45 | configatron.temp_end 46 | end 47 | 48 | class MyClass 49 | include Chalk::Log 50 | end 51 | 52 | describe 'when called without arguments' do 53 | it 'does not loop infinitely' do 54 | MyClass.log.info 55 | end 56 | end 57 | 58 | describe 'when called with a message' do 59 | it 'does not mutate the input' do 60 | canary = "'hello, world!'" 61 | baseline = canary.dup 62 | MyClass.log.info(canary) 63 | assert_equal(baseline, canary) 64 | end 65 | end 66 | 67 | describe 'layout' do 68 | before do 69 | @layout = MyClass::Layout.new 70 | end 71 | 72 | def layout(opts) 73 | event = Critic::Fake::Event.new(opts) 74 | formatted = @layout.format(event) 75 | 76 | # Everything should end with a newline, but they're annoying 77 | # to have to test elsewhere, so strip it away. 78 | assert_equal("\n", formatted[-1], "Layout did not end with a newline: #{formatted.inspect}") 79 | formatted.chomp 80 | end 81 | 82 | it 'log entry from info' do 83 | rendered = layout(data: ["A Message", {:key1 => "ValueOne", :key2 => ["An", "Array"]}]) 84 | assert_equal('[9973] A Message: key1=ValueOne key2=["An","Array"]', rendered) 85 | end 86 | 87 | it 'logs the message_prefix correctly' do 88 | Chalk::Log.with_message_prefix('PREFIX: ') do 89 | rendered = layout(data: ["A Message", {:key1 => "ValueOne", :key2 => ["An", "Array"]}]) 90 | assert_equal('[9973] PREFIX: A Message: key1=ValueOne key2=["An","Array"]', rendered) 91 | end 92 | end 93 | 94 | it 'logs the action_id correctly' do 95 | LSpace.with(action_id: 'action') do 96 | rendered = layout(data: ["A Message", {:key1 => "ValueOne", :key2 => ["An", "Array"]}]) 97 | assert_equal('[9973|action] A Message: key1=ValueOne key2=["An","Array"]', rendered) 98 | end 99 | end 100 | 101 | it 'logs timestamp correctly' do 102 | enable_timestamp 103 | LSpace.with(action_id: 'action') do 104 | rendered = layout(data: ["A Message", {:key1 => "ValueOne", :key2 => ["An", "Array"]}]) 105 | assert_equal('[1979-04-09 00:00:00.000000] [9973|action] A Message: key1=ValueOne key2=["An","Array"]', rendered) 106 | end 107 | end 108 | 109 | it 'logs without pid correctly' do 110 | disable_pid 111 | LSpace.with(action_id: 'action') do 112 | rendered = layout(data: ["A Message", {:key1 => "ValueOne", :key2 => ["An", "Array"]}]) 113 | assert_equal('[action] A Message: key1=ValueOne key2=["An","Array"]', rendered) 114 | end 115 | end 116 | 117 | it 'log from info hash without a message' do 118 | rendered = layout(data: [{:key1 => "ValueOne", :key2 => ["An", "Array"]}]) 119 | assert_equal('[9973] key1=ValueOne key2=["An","Array"]', rendered) 120 | end 121 | 122 | it 'renders [no backtrace] as appropriate' do 123 | rendered = layout(data: ["Another Message", StandardError.new('msg')]) 124 | assert_equal("[9973] Another Message: error_class=StandardError error=msg\n[9973] [no backtrace]", rendered) 125 | end 126 | 127 | it 'renders when given error and info hash' do 128 | rendered = layout(data: ["Another Message", StandardError.new('msg'), {:key1 => "ValueOne", :key2 => ["An", "Array"]}]) 129 | assert_equal(%Q{[9973] Another Message: key1=ValueOne key2=["An","Array"] error_class=StandardError error=msg\n[9973] [no backtrace]}, rendered) 130 | end 131 | 132 | it 'renders an error with a backtrace' do 133 | error = StandardError.new('msg') 134 | backtrace = ["a fake", "backtrace"] 135 | error.set_backtrace(backtrace) 136 | 137 | rendered = layout(data: ["Yet Another Message", error]) 138 | assert_equal("[9973] Yet Another Message: error_class=StandardError error=msg\n[9973] a fake\n[9973] backtrace", rendered) 139 | end 140 | 141 | it 'hides backtraces when they are disabled' do 142 | error = StandardError.new('msg') 143 | backtrace = ["a fake", "backtrace"] 144 | error.set_backtrace(backtrace) 145 | 146 | disable_backtraces 147 | rendered = layout(data: ['Even more messages', error]) 148 | assert_equal('[9973] Even more messages: error_class=StandardError error=msg', rendered) 149 | end 150 | 151 | it 'renders an error passed alone' do 152 | error = StandardError.new('msg') 153 | backtrace = ["a fake", "backtrace"] 154 | error.set_backtrace(backtrace) 155 | 156 | rendered = layout(data: [error]) 157 | assert_equal("[9973] error_class=StandardError error=msg\n[9973] a fake\n[9973] backtrace", rendered) 158 | end 159 | 160 | it 'handles bad unicode' do 161 | rendered = layout(data: [{:key1 => "ValueOne", :key2 => "\xC3"}]) 162 | assert_equal("[9973] key1=ValueOne key2=\"\\xC3\" [JSON-FAILED]", rendered) 163 | end 164 | 165 | it 'allows disabling tagging' do 166 | enable_timestamp 167 | disable_tagging 168 | 169 | LSpace.with(action_id: 'action') do 170 | rendered = layout(data: [{:key1 => "ValueOne", :key2 => "Value Two"}]) 171 | assert_equal(%Q{key1=ValueOne key2="Value Two"}, rendered) 172 | end 173 | end 174 | 175 | it 'logs spans correctly' do 176 | enable_timestamp 177 | TestSpan = Struct.new(:action_id, :span_id, :parent_id) do 178 | def to_s 179 | sprintf("%s %s>%s", 180 | action_id, 181 | parent_id.to_s(16).rjust(16,'0'), 182 | span_id.to_s(16).rjust(16,'0')) 183 | end 184 | end 185 | LSpace.with(span: TestSpan.new("action", 0, 0)) do 186 | rendered = layout(data: ["llamas"]) 187 | assert_equal('[1979-04-09 00:00:00.000000] [9973|action 0000000000000000>0000000000000000] llamas', rendered) 188 | end 189 | LSpace.with(span: TestSpan.new("action", 2, 0)) do 190 | rendered = layout(data: ["llamas"]) 191 | assert_equal('[1979-04-09 00:00:00.000000] [9973|action 0000000000000000>0000000000000002] llamas', rendered) 192 | end 193 | LSpace.with(span: TestSpan.new("action", 0, 123)) do 194 | rendered = layout(data: ["llamas"]) 195 | assert_equal('[1979-04-09 00:00:00.000000] [9973|action 000000000000007b>0000000000000000] llamas', rendered) 196 | end 197 | LSpace.with(span: TestSpan.new("action", 2, 123)) do 198 | rendered = layout(data: ["llamas"]) 199 | assert_equal('[1979-04-09 00:00:00.000000] [9973|action 000000000000007b>0000000000000002] llamas', rendered) 200 | end 201 | end 202 | 203 | describe 'faults' do 204 | it 'shows an appropriate error if the invalid arguments are provided' do 205 | rendered = layout(data: ['foo', nil]) 206 | 207 | lines = rendered.split("\n") 208 | assert_equal('[Chalk::Log fault: Could not format message] error_class="Chalk::Log::InvalidArguments" error="Invalid leftover arguments: [\"foo\", nil]"', lines[0]) 209 | assert(lines.length > 1) 210 | end 211 | 212 | it 'handles single faults' do 213 | e = StandardError.new('msg') 214 | @layout.expects(:do_format).raises(e) 215 | rendered = layout(data: ['hi']) 216 | 217 | lines = rendered.split("\n") 218 | assert_equal('[Chalk::Log fault: Could not format message] error_class=StandardError error=msg', lines[0]) 219 | assert(lines.length > 1) 220 | end 221 | 222 | it 'handles double-faults' do 223 | e = StandardError.new('msg') 224 | def e.to_s; raise 'Time to double-fault'; end 225 | 226 | @layout.expects(:do_format).raises(e) 227 | rendered = layout(data: ['hi']) 228 | 229 | lines = rendered.split("\n") 230 | assert_match(/Chalk::Log fault: Double fault while formatting message/, lines[0]) 231 | assert_equal(1, lines.length, "Lines: #{lines.inspect}") 232 | end 233 | 234 | it 'handles triple-faults' do 235 | e = StandardError.new('msg') 236 | def e.to_s 237 | f = StandardError.new('double') 238 | def f.to_s; raise 'Time to triple fault'; end 239 | raise f 240 | end 241 | 242 | @layout.expects(:do_format).raises(e) 243 | rendered = layout(data: ['hi']) 244 | 245 | lines = rendered.split("\n") 246 | assert_match(/Chalk::Log fault: Triple fault while formatting message/, lines[0]) 247 | assert_equal(1, lines.length, "Lines: #{lines.inspect}") 248 | end 249 | end 250 | end 251 | end 252 | end 253 | --------------------------------------------------------------------------------