berkeley_library-logging 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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