berkeley_library-logging 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +388 -0
  3. data/.idea/inspectionProfiles/Project_Default.xml +17 -0
  4. data/.idea/logging.iml +139 -0
  5. data/.idea/modules.xml +8 -0
  6. data/.idea/vcs.xml +6 -0
  7. data/.rubocop.yml +271 -0
  8. data/.ruby-version +1 -0
  9. data/.simplecov +4 -0
  10. data/.travis.yml +9 -0
  11. data/CHANGES.md +12 -0
  12. data/Dockerfile +57 -0
  13. data/Gemfile +3 -0
  14. data/Jenkinsfile +21 -0
  15. data/LICENSE.md +21 -0
  16. data/README.md +47 -0
  17. data/Rakefile +31 -0
  18. data/berkeley_library-logging.gemspec +47 -0
  19. data/docker-compose.yml +15 -0
  20. data/lib/berkeley_library/logging.rb +38 -0
  21. data/lib/berkeley_library/logging/configurator.rb +41 -0
  22. data/lib/berkeley_library/logging/env.rb +38 -0
  23. data/lib/berkeley_library/logging/events.rb +39 -0
  24. data/lib/berkeley_library/logging/formatters.rb +54 -0
  25. data/lib/berkeley_library/logging/logger.rb +12 -0
  26. data/lib/berkeley_library/logging/loggers.rb +79 -0
  27. data/lib/berkeley_library/logging/module_info.rb +16 -0
  28. data/lib/berkeley_library/logging/railtie.rb +15 -0
  29. data/lib/berkeley_library/logging/tagged_logging_extensions.rb +21 -0
  30. data/rakelib/.rubocop.yml +4 -0
  31. data/rakelib/bundle.rake +8 -0
  32. data/rakelib/coverage.rake +36 -0
  33. data/rakelib/gem.rake +56 -0
  34. data/rakelib/rubocop.rake +14 -0
  35. data/rakelib/spec.rake +31 -0
  36. data/spec/.rubocop.yml +27 -0
  37. data/spec/rails/ucblit/logging/configurator_spec.rb +101 -0
  38. data/spec/rails/ucblit/logging/env_spec.rb +25 -0
  39. data/spec/rails/ucblit/logging/formatters_spec.rb +44 -0
  40. data/spec/rails/ucblit/logging/loggers_spec.rb +117 -0
  41. data/spec/rails/ucblit/logging/railtie_spec.rb +46 -0
  42. data/spec/rails/ucblit/logging_spec.rb +132 -0
  43. data/spec/rails_helper.rb +15 -0
  44. data/spec/spec_helper.rb +34 -0
  45. data/spec/standalone/ucblit/logging/configurator_spec.rb +103 -0
  46. data/spec/standalone/ucblit/logging/formatters_spec.rb +44 -0
  47. data/spec/standalone/ucblit/logging/loggers_spec.rb +278 -0
  48. data/spec/standalone/ucblit/logging_spec.rb +133 -0
  49. data/spec/standalone_helper.rb +25 -0
  50. metadata +363 -0
data/Rakefile ADDED
@@ -0,0 +1,31 @@
1
+ ENV['BUNDLE_GEMFILE'] ||= File.expand_path('Gemfile', __dir__)
2
+ require 'bundler/setup' # Set up gems listed in the Gemfile.
3
+
4
+ # ------------------------------------------------------------
5
+ # Application code
6
+
7
+ File.expand_path('lib', __dir__).tap do |lib|
8
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
9
+ end
10
+
11
+ # ------------------------------------------------------------
12
+ # CI
13
+
14
+ if ENV['CI']
15
+ ENV['RAILS_ENV'] = 'test'
16
+ ENV['GENERATE_REPORTS'] ||= 'true'
17
+ end
18
+
19
+ # ------------------------------------------------------------
20
+ # Custom tasks
21
+
22
+ desc 'Remove artifacts directory'
23
+ task :clean do
24
+ FileUtils.rm_rf('artifacts')
25
+ end
26
+
27
+ desc 'Check test coverage, check code style, check gems for vulnerabilities'
28
+ task check: %w[coverage rubocop bundle:audit]
29
+
30
+ desc 'Clean, check, build gem'
31
+ task default: %i[clean check gem]
@@ -0,0 +1,47 @@
1
+ File.expand_path('lib', __dir__).tap do |lib|
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+ end
4
+
5
+ ruby_version = begin
6
+ ruby_version_file = File.expand_path('.ruby-version', __dir__)
7
+ File.read(ruby_version_file).strip
8
+ end
9
+
10
+ require 'berkeley_library/logging/module_info'
11
+
12
+ Gem::Specification.new do |spec|
13
+ spec.name = BerkeleyLibrary::Logging::ModuleInfo::NAME
14
+ spec.author = BerkeleyLibrary::Logging::ModuleInfo::AUTHOR
15
+ spec.email = BerkeleyLibrary::Logging::ModuleInfo::AUTHOR_EMAIL
16
+ spec.summary = BerkeleyLibrary::Logging::ModuleInfo::SUMMARY
17
+ spec.description = BerkeleyLibrary::Logging::ModuleInfo::DESCRIPTION
18
+ spec.license = BerkeleyLibrary::Logging::ModuleInfo::LICENSE
19
+ spec.version = BerkeleyLibrary::Logging::ModuleInfo::VERSION
20
+ spec.homepage = BerkeleyLibrary::Logging::ModuleInfo::HOMEPAGE
21
+
22
+ spec.files = `git ls-files -z`.split("\x0")
23
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
24
+ spec.require_paths = ['lib']
25
+
26
+ spec.required_ruby_version = ">= #{ruby_version}"
27
+
28
+ spec.add_dependency 'activesupport', '~> 6.0'
29
+ spec.add_dependency 'amazing_print', '~> 1.1'
30
+ spec.add_dependency 'lograge', '~> 0.11'
31
+ spec.add_dependency 'ougai', '~> 1.8'
32
+
33
+ spec.add_development_dependency 'brakeman', '~> 4.9'
34
+ spec.add_development_dependency 'bundle-audit', '~> 0.1'
35
+ spec.add_development_dependency 'ci_reporter_rspec', '~> 1.0'
36
+ spec.add_development_dependency 'colorize', '~> 0.8'
37
+ spec.add_development_dependency 'dotenv', '~> 2.7'
38
+ spec.add_development_dependency 'irb', '~> 1.2' # workaroundfor https://github.com/bundler/bundler/issues/6929
39
+ spec.add_development_dependency 'listen', '>= 3.0.5', '< 3.2'
40
+ spec.add_development_dependency 'rails', '~> 6.0'
41
+ spec.add_development_dependency 'rake', '~> 13.0'
42
+ spec.add_development_dependency 'rspec-support', '~> 3.9'
43
+ spec.add_development_dependency 'rubocop', '~> 0.91.0'
44
+ spec.add_development_dependency 'simplecov', '~> 0.21.1'
45
+ spec.add_development_dependency 'simplecov-console', '~> 0.9.1'
46
+ spec.add_development_dependency 'simplecov-rcov', '~> 0.2'
47
+ end
@@ -0,0 +1,15 @@
1
+ services:
2
+ gem:
3
+ build:
4
+ context: .
5
+ target: development
6
+ ports:
7
+ - target: 3000
8
+ published: 3000
9
+ restart: always
10
+ volumes:
11
+ # Note that this mounts the *entire* repo directory (including
12
+ # files ignored in .dockerignore when building the image)
13
+ - ./:/opt/app
14
+
15
+ version: "3.7"
@@ -0,0 +1,38 @@
1
+ if defined?(Rails)
2
+ require 'berkeley_library/logging/railtie'
3
+ else
4
+ require 'berkeley_library/logging/configurator'
5
+ end
6
+
7
+ module BerkeleyLibrary
8
+ # Include this module to get access to a shared global logger.
9
+ module Logging
10
+ def logger
11
+ Logging.logger
12
+ end
13
+
14
+ def logger=(v)
15
+ Logging.logger = v
16
+ end
17
+
18
+ class << self
19
+ def logger
20
+ @logger ||= BerkeleyLibrary::Logging::Loggers.default_logger
21
+ end
22
+
23
+ def logger=(v)
24
+ @logger = (ensure_logger(v) unless v.nil?)
25
+ end
26
+
27
+ private
28
+
29
+ LOG_METHODS = %i[debug info warn error].freeze
30
+
31
+ def ensure_logger(v)
32
+ return v if (missing = LOG_METHODS.reject { |m| v.respond_to?(m) }).empty?
33
+
34
+ raise ArgumentError, "Not a logger: #{v.inspect} does not respond to #{missing.join(', ')}"
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,41 @@
1
+ require 'berkeley_library/logging/env'
2
+ require 'berkeley_library/logging/events'
3
+ require 'berkeley_library/logging/formatters'
4
+ require 'berkeley_library/logging/loggers'
5
+
6
+ module BerkeleyLibrary
7
+ module Logging
8
+ class Configurator
9
+ class << self
10
+
11
+ def configure(config)
12
+ configure_lograge(config)
13
+
14
+ logger = Loggers.new_default_logger(config)
15
+ logger.info("Custom logger initialized for environment #{Logging.env.inspect}")
16
+ configure_webpacker(logger)
17
+ config.logger = logger
18
+ end
19
+
20
+ private
21
+
22
+ def configure_lograge(config)
23
+ return unless config.respond_to?(:lograge)
24
+
25
+ config.lograge.tap do |lograge|
26
+ lograge.enabled = true
27
+ lograge.custom_options = Events.extract_data_for_lograge
28
+ lograge.formatter = Formatters.lograge_formatter
29
+ end
30
+ end
31
+
32
+ def configure_webpacker(logger)
33
+ return unless defined?(::Webpacker)
34
+
35
+ logger.info('Using custom logger for Webpacker')
36
+ ::Webpacker::Instance.logger = logger
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,38 @@
1
+ require 'active_support/string_inquirer'
2
+
3
+ module BerkeleyLibrary
4
+ module Logging
5
+ class << self
6
+ FALLBACK_ENV = 'development'.freeze
7
+ ENV_PREDICATES = %i[production? test? development?].freeze
8
+ private_constant :ENV_PREDICATES
9
+
10
+ def env
11
+ return Rails.env if defined?(Rails)
12
+
13
+ @env ||= begin
14
+ # Note: can't just self.env= b/c it returns the wrong value -- see
15
+ # https://stackoverflow.com/q/65226532/27358
16
+ env = (ENV['RAILS_ENV'] || ENV['RACK_ENV'] || FALLBACK_ENV)
17
+ ensure_rails_env_like(env)
18
+ end
19
+ end
20
+
21
+ def env=(v)
22
+ if defined?(Rails)
23
+ Rails.env = v
24
+ else
25
+ @env = ensure_rails_env_like(v)
26
+ end
27
+ end
28
+
29
+ private
30
+
31
+ def ensure_rails_env_like(v)
32
+ return v if ENV_PREDICATES.all? { |p| v.respond_to?(p) }
33
+
34
+ ActiveSupport::StringInquirer.new(v)
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,39 @@
1
+ module BerkeleyLibrary
2
+ module Logging
3
+ module Events
4
+ class << self
5
+ def extract_data_for_lograge
6
+ ->(event) { extract_event_data(event) }
7
+ end
8
+
9
+ private
10
+
11
+ def extract_event_data(event)
12
+ event_data = { time: Time.now }
13
+ extracted_headers = extract_headers(event)
14
+ event_data.merge(extracted_headers)
15
+ end
16
+
17
+ def extract_headers(event)
18
+ return {} unless (headers = event.payload[:headers])
19
+
20
+ extracted_headers = {
21
+ # yes, RFC 2616 uses a variant spelling for 'referrer', it's a known issue
22
+ # https://tools.ietf.org/html/rfc2616#section-14.36
23
+ referer: headers['HTTP_REFERER'],
24
+ request_id: headers['action_dispatch.request_id'],
25
+ remote_ip: headers['action_dispatch.remote_ip'],
26
+ remote_addr: headers['REMOTE_ADDR'],
27
+ x_forwarded_for: headers['HTTP_X_FORWARDED_FOR'],
28
+ forwarded: headers['HTTP_FORWARDED'] # RFC 7239
29
+ }
30
+
31
+ # Some of these 'headers' include recursive structures
32
+ # that cause SystemStackErrors in JSON serialization,
33
+ # so we convert them all to strings
34
+ extracted_headers.transform_values(&:to_s)
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,54 @@
1
+ require 'ougai'
2
+
3
+ module BerkeleyLibrary
4
+ module Logging
5
+ module Formatters
6
+
7
+ class << self
8
+ def new_json_formatter
9
+ Bunyan.new
10
+ end
11
+
12
+ def new_readable_formatter
13
+ Ougai::Formatters::Readable.new
14
+ end
15
+
16
+ def lograge_formatter
17
+ ->(data) { { msg: 'Request', request: Formatters.ensure_hash(data) } }
18
+ end
19
+
20
+ def ensure_hash(message)
21
+ return {} unless message
22
+ return message if message.is_a?(Hash)
23
+
24
+ { msg: message }
25
+ end
26
+ end
27
+
28
+ # ------------------------------------------------------------
29
+ # Private helper classes
30
+
31
+ class Bunyan < Ougai::Formatters::Bunyan
32
+ include Ougai::Logging::Severity
33
+
34
+ def _call(severity, time, progname, data)
35
+ original_data = Formatters.ensure_hash(data)
36
+
37
+ # Ougai::Formatters::Bunyan replaces the human-readable severity string
38
+ # with a numeric level, so we add it here as a separate attribute
39
+ severity = ensure_human_readable(severity)
40
+ merged_data = { severity: severity }.merge(original_data)
41
+ super(severity, time, progname, merged_data)
42
+ end
43
+
44
+ def ensure_human_readable(severity)
45
+ return to_label(severity) if severity.is_a?(Integer)
46
+
47
+ severity.to_s
48
+ end
49
+ end
50
+
51
+ private_constant :Bunyan
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,12 @@
1
+ require 'active_support/logger'
2
+ require 'ougai'
3
+ require 'berkeley_library/logging/tagged_logging_extensions'
4
+
5
+ module BerkeleyLibrary
6
+ module Logging
7
+ class Logger < Ougai::Logger
8
+ include ActiveSupport::LoggerThreadSafeLevel
9
+ include ActiveSupport::LoggerSilence
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,79 @@
1
+ require 'berkeley_library/logging/env'
2
+ require 'berkeley_library/logging/formatters'
3
+ require 'berkeley_library/logging/logger'
4
+
5
+ module BerkeleyLibrary
6
+ module Logging
7
+ module Loggers
8
+ class << self
9
+ FALLBACK_LOG_DIR = 'log'.freeze
10
+
11
+ def default_logger
12
+ if defined?(Rails)
13
+ return Rails.logger if Rails.logger
14
+
15
+ warn('Rails is defined, but Rails logger is nil')
16
+ end
17
+
18
+ new_default_logger
19
+ end
20
+
21
+ # TODO: support passing a hash / passing default_log_file
22
+ def new_default_logger(config = nil)
23
+ return new_readable_logger($stdout) unless config
24
+ return new_json_logger($stdout) if Logging.env.production?
25
+ return rails_file_logger(config) if Logging.env.test?
26
+ return new_broadcast_logger(config) if Logging.env.development?
27
+
28
+ raise ArgumentError, "Can't create logger for environment: #{Logging.env.inspect}"
29
+ end
30
+
31
+ def new_json_logger(logdev)
32
+ new_logger_with(logdev: logdev, formatter: Formatters.new_json_formatter)
33
+ end
34
+
35
+ def new_readable_logger(logdev)
36
+ new_logger_with(logdev: logdev, formatter: Formatters.new_readable_formatter)
37
+ end
38
+
39
+ private
40
+
41
+ def new_broadcast_logger(config)
42
+ new_json_logger($stdout).tap do |json_logger|
43
+ file_logger = rails_file_logger(config)
44
+ json_logger.extend Ougai::Logger.broadcast(file_logger)
45
+ end
46
+ end
47
+
48
+ def rails_file_logger(config)
49
+ log_file = default_log_file_for(config)
50
+ new_readable_logger(log_file)
51
+ end
52
+
53
+ def new_logger_with(logdev:, formatter:)
54
+ Logger.new(logdev).tap { |l| l.formatter = formatter }
55
+ end
56
+
57
+ def default_log_file_for(config)
58
+ return config.default_log_file if config.respond_to?(:default_log_file)
59
+
60
+ File.join(ensure_log_directory, "#{Logging.env}.log")
61
+ end
62
+
63
+ def ensure_log_directory
64
+ File.join(workdir, FALLBACK_LOG_DIR).tap do |log_dir|
65
+ FileUtils.mkdir(log_dir) unless File.exist?(log_dir)
66
+ raise ArgumentError, "Not a directory: #{log_dir}" unless File.directory?(log_dir)
67
+ end
68
+ end
69
+
70
+ def workdir
71
+ return Rails.application.root if defined?(Rails)
72
+
73
+ Pathname.getwd
74
+ end
75
+
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,16 @@
1
+ module BerkeleyLibrary
2
+ module Logging
3
+ class ModuleInfo
4
+ NAME = 'berkeley_library-logging'.freeze
5
+ AUTHOR = 'David Moles'.freeze
6
+ AUTHOR_EMAIL = 'dmoles@berkeley.edu'.freeze
7
+ SUMMARY = 'Opinionated Ruby/Rails logging for UC Berkeley Library'.freeze
8
+ DESCRIPTION = 'A gem providing shared logging code for UC Berkeley Library gems and Rails applications'.freeze
9
+ LICENSE = 'MIT'.freeze
10
+ VERSION = '0.2.0'.freeze
11
+ HOMEPAGE = 'https://github.com/BerkeleyLibrary/logging'.freeze
12
+
13
+ private_class_method :new
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,15 @@
1
+ require 'lograge'
2
+ require 'lograge/railtie' # NOTE: registers Lograge::Railtie by side effect
3
+ require 'berkeley_library/logging/configurator'
4
+
5
+ module BerkeleyLibrary
6
+ module Logging
7
+ class Railtie < Rails::Railtie
8
+ # Don't use the Railtie's own `config` because configure() needs
9
+ # Rails::Application::Configuration#default_log_file
10
+ initializer('logging.configure_berkeley_library_logging', before: :initialize_logger) do |app|
11
+ Configurator.configure(app.config)
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,21 @@
1
+ require 'active_support/tagged_logging'
2
+ require 'berkeley_library/logging/formatters'
3
+
4
+ # Monkey-patch ActiveSupport::TaggedLogging::Formatter
5
+ # not to produce garbage by prepending tags to hashes.
6
+ #
7
+ # TODO: Can we intercept Formatter.extend() instead? See
8
+ # https://github.com/rails/rails/blob/v6.0.3.4/activesupport/lib/active_support/tagged_logging.rb#L73
9
+ module ActiveSupport
10
+ module TaggedLogging
11
+ module Formatter
12
+ def call(severity, time, progname, data)
13
+ return super unless current_tags.present?
14
+
15
+ original_data = BerkeleyLibrary::Logging::Formatters.ensure_hash(data)
16
+ merged_data = { tags: current_tags }.merge(original_data)
17
+ super(severity, time, progname, merged_data)
18
+ end
19
+ end
20
+ end
21
+ end