paul_bunyan 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (83) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +24 -0
  3. data/.travis.yml +9 -0
  4. data/Dockerfile +13 -0
  5. data/Gemfile +6 -0
  6. data/Guardfile +16 -0
  7. data/LICENSE.txt +22 -0
  8. data/README.md +51 -0
  9. data/README.rdoc +3 -0
  10. data/Rakefile +19 -0
  11. data/bin/logging_demo +17 -0
  12. data/build.sh +7 -0
  13. data/docker-compose.yml +4 -0
  14. data/lib/paul_bunyan.rb +70 -0
  15. data/lib/paul_bunyan/json_formatter.rb +122 -0
  16. data/lib/paul_bunyan/level.rb +28 -0
  17. data/lib/paul_bunyan/log_relayer.rb +148 -0
  18. data/lib/paul_bunyan/rails_ext.rb +7 -0
  19. data/lib/paul_bunyan/rails_ext/instrumentation.rb +41 -0
  20. data/lib/paul_bunyan/rails_ext/rack_logger.rb +24 -0
  21. data/lib/paul_bunyan/railtie.rb +75 -0
  22. data/lib/paul_bunyan/railtie/log_subscriber.rb +182 -0
  23. data/lib/paul_bunyan/text_formatter.rb +11 -0
  24. data/lib/paul_bunyan/version.rb +3 -0
  25. data/lib/tasks/paul_bunyan_tasks.rake +4 -0
  26. data/paul_bunyan.gemspec +30 -0
  27. data/spec/dummy/Rakefile +6 -0
  28. data/spec/dummy/app/assets/images/.keep +0 -0
  29. data/spec/dummy/app/assets/javascripts/application.js +13 -0
  30. data/spec/dummy/app/assets/stylesheets/application.css +15 -0
  31. data/spec/dummy/app/controllers/application_controller.rb +5 -0
  32. data/spec/dummy/app/controllers/concerns/.keep +0 -0
  33. data/spec/dummy/app/helpers/application_helper.rb +2 -0
  34. data/spec/dummy/app/mailers/.keep +0 -0
  35. data/spec/dummy/app/models/.keep +0 -0
  36. data/spec/dummy/app/models/concerns/.keep +0 -0
  37. data/spec/dummy/app/views/layouts/application.html.erb +14 -0
  38. data/spec/dummy/bin/bundle +3 -0
  39. data/spec/dummy/bin/rails +4 -0
  40. data/spec/dummy/bin/rake +4 -0
  41. data/spec/dummy/bin/setup +29 -0
  42. data/spec/dummy/config.ru +4 -0
  43. data/spec/dummy/config/application.rb +28 -0
  44. data/spec/dummy/config/boot.rb +5 -0
  45. data/spec/dummy/config/database.yml +25 -0
  46. data/spec/dummy/config/environment.rb +5 -0
  47. data/spec/dummy/config/environments/development.rb +41 -0
  48. data/spec/dummy/config/environments/production.rb +79 -0
  49. data/spec/dummy/config/environments/test.rb +42 -0
  50. data/spec/dummy/config/initializers/assets.rb +11 -0
  51. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  52. data/spec/dummy/config/initializers/cookies_serializer.rb +3 -0
  53. data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  54. data/spec/dummy/config/initializers/inflections.rb +16 -0
  55. data/spec/dummy/config/initializers/mime_types.rb +4 -0
  56. data/spec/dummy/config/initializers/secret_token.rb +1 -0
  57. data/spec/dummy/config/initializers/session_store.rb +3 -0
  58. data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
  59. data/spec/dummy/config/locales/en.yml +23 -0
  60. data/spec/dummy/config/routes.rb +56 -0
  61. data/spec/dummy/config/secrets.yml +22 -0
  62. data/spec/dummy/lib/assets/.keep +0 -0
  63. data/spec/dummy/log/.keep +0 -0
  64. data/spec/dummy/public/404.html +67 -0
  65. data/spec/dummy/public/422.html +67 -0
  66. data/spec/dummy/public/500.html +66 -0
  67. data/spec/dummy/public/favicon.ico +0 -0
  68. data/spec/gemfiles/40.gemfile +5 -0
  69. data/spec/gemfiles/40.gemfile.lock +137 -0
  70. data/spec/gemfiles/41.gemfile +5 -0
  71. data/spec/gemfiles/41.gemfile.lock +142 -0
  72. data/spec/gemfiles/42.gemfile +5 -0
  73. data/spec/gemfiles/42.gemfile.lock +167 -0
  74. data/spec/lib/paul_bunyan/json_formatter_spec.rb +237 -0
  75. data/spec/lib/paul_bunyan/level_spec.rb +78 -0
  76. data/spec/lib/paul_bunyan/log_relayer_spec.rb +333 -0
  77. data/spec/lib/paul_bunyan/rails_ext/instrumentation_spec.rb +81 -0
  78. data/spec/lib/paul_bunyan/railtie/log_subscriber_spec.rb +304 -0
  79. data/spec/lib/paul_bunyan/railtie_spec.rb +37 -0
  80. data/spec/lib/paul_bunyan_spec.rb +137 -0
  81. data/spec/spec_helper.rb +24 -0
  82. data/spec/support/notification_helpers.rb +22 -0
  83. metadata +303 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 0c8be4508c3bf748cc21adad0846a24d822dbea2
4
+ data.tar.gz: bee128e2be543d84c4ae31571949385c53c74a6f
5
+ SHA512:
6
+ metadata.gz: 49db1d3690ffdf9756b57200227e1ddb797dd867064fb3c4f63e6264e1babd8737473986abd01497a02a22b962ad17bcbfe4a4ad91ec7431c499307304c8363d
7
+ data.tar.gz: a41bb6c373d12cc1a4d68e0c38524aa5bea49a6e8adc175f439fdd17c7dcf9f53e46c39f03a8a7b279702b8c9c08efa7f72a103e79975cbcb7b9fb997c6936be
data/.gitignore ADDED
@@ -0,0 +1,24 @@
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
+ log/*.log
19
+ pkg/
20
+ spec/dummy/db/*.sqlite3
21
+ spec/dummy/db/*.sqlite3-journal
22
+ spec/dummy/log/*.log
23
+ spec/dummy/tmp/
24
+ spec/dummy/.sass-cache
data/.travis.yml ADDED
@@ -0,0 +1,9 @@
1
+ bundler_args: ''
2
+ language: ruby
3
+ gemfile:
4
+ - spec/gemfiles/40.gemfile
5
+ - spec/gemfiles/41.gemfile
6
+ - spec/gemfiles/42.gemfile
7
+ matrix:
8
+ exclude:
9
+ script: bundle exec rspec
data/Dockerfile ADDED
@@ -0,0 +1,13 @@
1
+ FROM docker.insops.net/instructure/instructure-ruby:2.1
2
+ MAINTAINER Tyler Pickett <tpickett@instructure.com>
3
+
4
+ WORKDIR /usr/src/app
5
+ COPY Gemfile* *.gemspec /usr/src/app/
6
+ COPY lib/paul_bunyan/version.rb /usr/src/app/lib/paul_bunyan/
7
+ RUN bundle install
8
+
9
+ COPY . /usr/src/app
10
+ USER root
11
+ RUN chown -R docker:docker /usr/src/app/*
12
+ USER docker
13
+ CMD bundle exec wwtd --parallel
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Declare your gem's dependencies in logging.gemspec.
4
+ # Bundler will treat runtime dependencies like base dependencies, and
5
+ # development dependencies will be added by default to the :development group.
6
+ gemspec
data/Guardfile ADDED
@@ -0,0 +1,16 @@
1
+ # A sample Guardfile
2
+ # More info at https://github.com/guard/guard#readme
3
+
4
+ # Note: The cmd option is now required due to the increasing number of ways
5
+ # rspec may be run, below are examples of the most common uses.
6
+ # * bundler: 'bundle exec rspec'
7
+ # * bundler binstubs: 'bin/rspec'
8
+ # * spring: 'bin/rsspec' (This will use spring if running and you have
9
+ # installed the spring binstubs per the docs)
10
+ # * zeus: 'zeus rspec' (requires the server to be started separetly)
11
+ # * 'just' rspec: 'rspec'
12
+ guard :rspec, cmd: 'bundle exec rspec' do
13
+ watch(%r{^spec/.+_spec\.rb$})
14
+ watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
15
+ watch('spec/spec_helper.rb') { "spec" }
16
+ end
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Instructure Inc.
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.
data/README.md ADDED
@@ -0,0 +1,51 @@
1
+ # Paul Bunyan (formerly: Logging)
2
+
3
+ PaulBunyan is a re-usable component with a globally accessible Logger with extra
4
+ support for handling logging in Rails.
5
+
6
+ ```
7
+ class Foo
8
+ include PaulBunyan
9
+
10
+ def bar
11
+ logger.warn "blah"
12
+ end
13
+ end
14
+ ```
15
+
16
+ Also included is a Railtie that overrides the default rails logger to always
17
+ print to STDOUT as well as format the messages to JSON for machine readable
18
+ goodness. This has been tested with Rails 4.2 but should compatible with
19
+ Rails 3 or newer since that's when Railties were introduced.
20
+
21
+ ## Installation
22
+
23
+ Add this line to your application's Gemfile:
24
+
25
+ gem 'paul_bunyan'
26
+
27
+ And then execute:
28
+
29
+ $ bundle
30
+
31
+ Or install it yourself as:
32
+
33
+ $ gem install paul_bunyan
34
+
35
+ ## Usage
36
+
37
+ ### Non-Rails projects:
38
+
39
+ ```
40
+ require 'paul_bunyan'
41
+
42
+ include PaulBunyan::Logger
43
+
44
+ PaulBunyan.set_logger(STDOUT)
45
+
46
+ logger.warn "blah"
47
+ ```
48
+
49
+ ### Rails projects:
50
+
51
+ Nothing after it's added to your Gemfile, the Railtie takes care of the rest.
data/README.rdoc ADDED
@@ -0,0 +1,3 @@
1
+ = PaulBunyan
2
+
3
+ This project rocks and uses MIT-LICENSE.
data/Rakefile ADDED
@@ -0,0 +1,19 @@
1
+ begin
2
+ require 'bundler/setup'
3
+ rescue LoadError
4
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
5
+ end
6
+ require "wwtd/tasks"
7
+ require 'rdoc/task'
8
+
9
+ RDoc::Task.new(:rdoc) do |rdoc|
10
+ rdoc.rdoc_dir = 'rdoc'
11
+ rdoc.title = 'PaulBunyan'
12
+ rdoc.options << '--line-numbers'
13
+ rdoc.rdoc_files.include('README.rdoc')
14
+ rdoc.rdoc_files.include('lib/**/*.rb')
15
+ end
16
+
17
+ Bundler::GemHelper.install_tasks
18
+
19
+ task default: "wwtd"
data/bin/logging_demo ADDED
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'bundler/setup'
4
+ require_relative '../lib/logging'
5
+
6
+ logger = PaulBunyan.set_logger(STDOUT)
7
+ puts "JSONFormatter"
8
+ logger.debug("PaulBunyanDemo") { "This is my debug message" }
9
+ logger.info("PaulBunyanDemo") { "This is my info message" }
10
+ logger.info("PaulBunyanDemo") { {foo: "bar", baz: "qux"} }
11
+ logger.warn("PaulBunyanDemo") { "This is my warn message" }
12
+ begin
13
+ raise "heck"
14
+ rescue => ex
15
+ logger.error("PaulBunyanDemo") { ex }
16
+ end
17
+ logger.fatal("PaulBunyanDemo") { "This is my fatal message" }
data/build.sh ADDED
@@ -0,0 +1,7 @@
1
+ #!/bin/bash
2
+
3
+ set -e
4
+
5
+ docker pull docker.insops.net/instructure/instructure-ruby:2.1
6
+ docker-compose build
7
+ docker-compose run test
@@ -0,0 +1,4 @@
1
+ test:
2
+ build: .
3
+ volumes:
4
+ - "coverage:/app/coverage"
@@ -0,0 +1,70 @@
1
+ require 'logger'
2
+
3
+ require_relative 'paul_bunyan/json_formatter'
4
+ require_relative 'paul_bunyan/level'
5
+ require_relative 'paul_bunyan/log_relayer'
6
+ require_relative 'paul_bunyan/text_formatter'
7
+ require_relative 'paul_bunyan/version'
8
+
9
+ require_relative 'paul_bunyan/railtie' if defined? ::Rails::Railtie
10
+
11
+ # Example Usage:
12
+ #
13
+ # class MyClass
14
+ # include PaulBunyan
15
+ #
16
+ # def initialize
17
+ # logger.info{ "something is working!" }
18
+ # end
19
+ # end
20
+ #
21
+ module PaulBunyan
22
+ class Error < StandardError; end
23
+ class UnknownFormatterError < Error; end
24
+ class UnknownLevelError < Error; end
25
+
26
+ # The ONE method we care about.
27
+ def logger
28
+ PaulBunyan.logger
29
+ end
30
+
31
+ class << self
32
+ attr_accessor :default_formatter_type
33
+ PaulBunyan.default_formatter_type = :json
34
+
35
+ def logger
36
+ create_logger(STDOUT) unless @logger
37
+ @logger
38
+ end
39
+
40
+ def create_logger(logdev, shift_age = 0, shift_size = 1048576, formatter_type: PaulBunyan.default_formatter_type)
41
+ logger = Logger.new(logdev, shift_age, shift_size)
42
+ logger.formatter = default_formatter(formatter_type) unless formatter_type.nil?
43
+ add_logger(logger)
44
+ end
45
+
46
+ def add_logger(logger)
47
+ @logger ||= LogRelayer.new
48
+ @logger.add_logger(logger)
49
+ logger
50
+ end
51
+
52
+ def remove_logger(logger)
53
+ @logger.remove_logger(logger)
54
+ end
55
+
56
+ private def default_formatter(formatter_type)
57
+ case formatter_type
58
+ when :json
59
+ JSONFormatter.new
60
+ when :text
61
+ TextFormatter.new
62
+ else
63
+ fail UnknownFormatterError, "Unknown formatter type #{formatter_type}"
64
+ end
65
+ end
66
+ end
67
+ end
68
+
69
+ # For backwards compatibility with applications that used this prior to the rename
70
+ Logging = PaulBunyan
@@ -0,0 +1,122 @@
1
+ require 'json'
2
+ require 'thread'
3
+
4
+ module PaulBunyan
5
+ class JSONFormatter
6
+ DATETIME_FORMAT = '%Y-%m-%dT%H:%M:%S.%3N'
7
+
8
+ def call(severity, time, progname, msg)
9
+ metadata = {
10
+ "ts" => time.utc.strftime(DATETIME_FORMAT),
11
+ "unix_ts" => time.to_f,
12
+ "severity" => severity,
13
+ "pid" => $$,
14
+ }
15
+ metadata['program'] = progname if progname
16
+ metadata['tags'] = current_tags unless current_tags.empty?
17
+
18
+ message_data = format_message(msg)
19
+
20
+ JSON::generate(merge_metadata_and_message(metadata, message_data)) + "\n"
21
+ end
22
+
23
+ def clear_tags!
24
+ current_tags.clear
25
+ end
26
+
27
+ def current_tags
28
+ thread_key = @thread_key ||= "logging_tagged_logging_tags:#{ Thread.current.object_id }"
29
+ Thread.current[thread_key] ||= []
30
+ end
31
+
32
+ def datetime_format=(value)
33
+ # intentional nop because the whole point of this formatter is
34
+ # to have a consistent machine parsable format :-P
35
+ end
36
+
37
+ def datetime_format
38
+ DATETIME_FORMAT
39
+ end
40
+
41
+ def pop_tags(count = 1)
42
+ current_tags.pop(count)
43
+ end
44
+
45
+ def push_tags(*tags)
46
+ tags.flatten.reject{|t| t.nil? || t.to_s.strip == '' }.tap do |clean_tags|
47
+ current_tags.concat(clean_tags)
48
+ end
49
+ end
50
+
51
+ def tagged(*tags)
52
+ clean_tags = push_tags(tags)
53
+ yield
54
+ ensure
55
+ pop_tags(clean_tags.size)
56
+ end
57
+
58
+ private
59
+
60
+ # TODO: extract all of this formatting/merging out into another class if it grows
61
+ def format_message(message)
62
+ case message
63
+ when Exception
64
+ format_exception(message)
65
+ when String
66
+ format_string(message)
67
+ else
68
+ format_generic_object(message)
69
+ end
70
+ end
71
+
72
+ def format_exception(exception)
73
+ # TODO: capture the exception cause if it is present and handle the case where
74
+ # cause isn't actually an exception (such as Parslet::ParseFailed#cause)
75
+ {
76
+ "exception.class" => exception.class.to_s,
77
+ "exception.backtrace" => exception.backtrace,
78
+ "exception.message" => exception.message,
79
+ }
80
+ end
81
+
82
+ def format_string(message)
83
+ { "message" => strip_ansi(message.strip) }
84
+ end
85
+
86
+ def format_generic_object(object)
87
+ if object.respond_to?(:to_h)
88
+ object.to_h
89
+ elsif object.respond_to?(:to_hash)
90
+ object.to_hash
91
+ else
92
+ format_string(object.inspect)
93
+ end
94
+ end
95
+
96
+ def merge_metadata_and_message(metadata, message)
97
+ clean_message = sanitize_message_keys(message, metadata.keys)
98
+ metadata.merge(clean_message)
99
+ end
100
+
101
+ def sanitize_message_keys(message, metadata_keys)
102
+ message.inject({}) { |clean, (key, value)|
103
+ key = key.to_s
104
+ if metadata_keys.include?(key)
105
+ clean["user.#{ key }"] = value
106
+ else
107
+ clean[key] = value
108
+ end
109
+ clean
110
+ }
111
+ end
112
+
113
+ ANSI_REGEX = /(?:\e\[|\u009b)(?:\d{1,3}(?:;\d{0,3})*)?[0-9A-MRcf-npqrsuy]/
114
+ def strip_ansi(value)
115
+ if value.respond_to?(:to_str)
116
+ value.to_str.gsub(ANSI_REGEX, '')
117
+ elsif value
118
+ value.gsub(ANSI_REGEX, '')
119
+ end
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,28 @@
1
+ module PaulBunyan
2
+ module Level
3
+ LEVEL_MAP = {
4
+ DEBUG: Logger::DEBUG,
5
+ INFO: Logger::INFO,
6
+ WARN: Logger::WARN,
7
+ ERROR: Logger::ERROR,
8
+ FATAL: Logger::FATAL,
9
+ UNKNOWN: Logger::UNKNOWN
10
+ }.freeze
11
+ LOGGING_LEVEL_KEYS = LEVEL_MAP.keys.freeze
12
+ LOGGING_LEVELS = (Logger::DEBUG..Logger::UNKNOWN).freeze
13
+
14
+ def self.coerce_level(level)
15
+ coerced_level = level || Logger::DEBUG
16
+ if level =~ /\A\s*\d+\s*\z/
17
+ coerced_level = level.to_i
18
+ elsif level.is_a?(String) || level.is_a?(Symbol)
19
+ coerced_level = LEVEL_MAP[level.upcase.to_sym]
20
+ end
21
+
22
+ unless LOGGING_LEVELS.cover?(coerced_level)
23
+ fail UnknownLevelError, "Unknown logging level #{level}. Please try one of: #{LOGGING_LEVEL_KEYS.join(', ')}."
24
+ end
25
+ coerced_level
26
+ end
27
+ end
28
+ end