green_log 0.1.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.
@@ -0,0 +1,21 @@
1
+ #! /usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "bundler/setup"
5
+
6
+ require "green_log/logger"
7
+ require "green_log/json_writer"
8
+ require "green_log/simple_writer"
9
+ require "pry"
10
+
11
+ case ARGV.first
12
+ when "json"
13
+ writer_class = GreenLog::JsonWriter
14
+ when "simple", nil
15
+ writer_class = GreenLog::SimpleWriter
16
+ else
17
+ raise ArgumentError, "bad format: #{ARGV.first}"
18
+ end
19
+
20
+ logger = GreenLog::Logger.new(writer_class.new(STDOUT))
21
+ logger.pry
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ lib = File.expand_path("lib", __dir__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+ require "green_log/version"
6
+
7
+ Gem::Specification.new do |spec|
8
+ spec.name = "green_log"
9
+ spec.version = GreenLog::VERSION
10
+ spec.authors = ["Mike Williams"]
11
+ spec.email = ["mike.williams@greensync.com.au"]
12
+
13
+ spec.summary = "Structured logging for cloud-native systems."
14
+ spec.homepage = "https://github.com/greensync/green_log"
15
+ spec.license = "MIT"
16
+
17
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
18
+ Dir.glob("{bin,doc,examples,lib}/**/*") + %w[
19
+ README.md green_log.gemspec
20
+ ]
21
+ end
22
+
23
+ spec.bindir = "exe"
24
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
25
+ spec.require_paths = ["lib"]
26
+
27
+ spec.add_runtime_dependency "values", "~> 1.8"
28
+ spec.add_development_dependency "bundler", "~> 2.0"
29
+ spec.add_development_dependency "pry", "~> 0.12.2"
30
+ spec.add_development_dependency "rack"
31
+ spec.add_development_dependency "rake", "~> 10.0"
32
+ spec.add_development_dependency "rspec", "~> 3.0"
33
+ spec.add_development_dependency "rubocop", "~> 0.77"
34
+
35
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "green_log/json_writer"
4
+ require "green_log/logger"
5
+ require "green_log/simple_writer"
6
+ require "green_log/version"
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "green_log/contextualizer"
4
+ require "green_log/entry"
5
+ require "green_log/severity"
6
+ require "green_log/severity_threshold_support"
7
+
8
+ module GreenLog
9
+
10
+ # An alternative to `GreenLog::Logger` for older code, which implements the
11
+ # same interface as the built-in Ruby `::Logger`.
12
+ class ClassicLogger
13
+
14
+ def initialize(downstream)
15
+ @downstream = downstream
16
+ end
17
+
18
+ attr_reader :downstream
19
+
20
+ include SeverityThresholdSupport
21
+
22
+ def add(severity, message = :unspecified, &block)
23
+ severity = Integer(severity)
24
+ return if severity < severity_threshold
25
+
26
+ entry = Entry.build(severity, resolve_message(message, &block))
27
+
28
+ downstream << entry
29
+
30
+ true
31
+ end
32
+
33
+ Severity::NAMES.each_with_index do |name, severity|
34
+
35
+ define_method(name.downcase) do |message = :unspecified, &block|
36
+ add(severity, message, &block)
37
+ end
38
+
39
+ define_method("#{name.downcase}?") do
40
+ severity >= severity_threshold
41
+ end
42
+
43
+ end
44
+
45
+ private
46
+
47
+ def resolve_message(message, &block)
48
+ normalise_message(extract_message(message, &block))
49
+ end
50
+
51
+ def extract_message(message, &block)
52
+ if block
53
+ raise ArgumentError, "both message and block provided" unless message == :unspecified
54
+
55
+ return block.call
56
+ end
57
+ raise ArgumentError, "no message provided" if message == :unspecified
58
+
59
+ message
60
+ end
61
+
62
+ def normalise_message(message)
63
+ return message if message.is_a?(Exception)
64
+ return message.to_str if message.respond_to?(:to_str)
65
+
66
+ message.inspect
67
+ end
68
+
69
+ end
70
+
71
+ # :rubocop:disable: Style/Documentation
72
+ class Logger
73
+
74
+ def to_classic_logger
75
+ GreenLog::ClassicLogger.new(downstream)
76
+ end
77
+
78
+ end
79
+
80
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "green_log/severity_threshold_support"
4
+
5
+ module GreenLog
6
+
7
+ # Log middleware that adds context.
8
+ class Contextualizer
9
+
10
+ def initialize(downstream, &context_generator)
11
+ @downstream = downstream
12
+ @context_generator = context_generator
13
+ end
14
+
15
+ attr_reader :downstream
16
+ attr_reader :context_generator
17
+
18
+ def <<(entry)
19
+ downstream << entry.in_context(context_generator.call)
20
+ end
21
+
22
+ include SeverityThresholdSupport
23
+
24
+ end
25
+
26
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GreenLog
4
+
5
+ # Refine
6
+ module CoreRefinements
7
+
8
+ refine ::Hash do
9
+
10
+ def to_loggable_value
11
+ {}.tap do |result|
12
+ each do |k, v|
13
+ result[k.to_sym] = v.to_loggable_value
14
+ end
15
+ end.freeze
16
+ end
17
+
18
+ def integrate(other)
19
+ other = other.to_hash
20
+ merge(other) do |_key, old_value, new_value|
21
+ if old_value.is_a?(Hash) && new_value.is_a?(Hash)
22
+ old_value.integrate(new_value)
23
+ else
24
+ new_value
25
+ end
26
+ end
27
+ end
28
+
29
+ end
30
+
31
+ refine ::Numeric do
32
+
33
+ def to_loggable_value
34
+ self
35
+ end
36
+
37
+ end
38
+
39
+ refine ::String do
40
+
41
+ def to_loggable_value
42
+ frozen? ? self : dup.freeze
43
+ end
44
+
45
+ end
46
+
47
+ refine ::NilClass do
48
+
49
+ def to_loggable_value
50
+ self
51
+ end
52
+
53
+ end
54
+
55
+ refine ::TrueClass do
56
+
57
+ def to_loggable_value
58
+ self
59
+ end
60
+
61
+ end
62
+
63
+ refine ::FalseClass do
64
+
65
+ def to_loggable_value
66
+ self
67
+ end
68
+
69
+ end
70
+
71
+ end
72
+
73
+ end
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "green_log/core_refinements"
4
+ require "green_log/severity"
5
+ require "values"
6
+
7
+ module GreenLog
8
+
9
+ # Represents a structured log entry.
10
+ class Entry < Value.new(:severity, :message, :context, :data, :exception)
11
+
12
+ using CoreRefinements
13
+
14
+ class << self
15
+
16
+ def with(**args)
17
+ args[:severity] = Severity.resolve(
18
+ args.fetch(:severity, Severity::INFO),
19
+ )
20
+ args[:message] ||= nil
21
+ args[:context] = args.fetch(:context, {}).to_loggable_value
22
+ args[:data] = args.fetch(:data, {}).to_loggable_value
23
+ args[:exception] ||= nil
24
+ super(**args)
25
+ end
26
+
27
+ def build(severity, *args, &block)
28
+ Builder.new(severity).build(*args, &block)
29
+ end
30
+
31
+ end
32
+
33
+ def in_context(extra_context)
34
+ with(context: extra_context.integrate(context).to_loggable_value)
35
+ end
36
+
37
+ # A builder for entries.
38
+ class Builder
39
+
40
+ def initialize(severity)
41
+ @severity = severity
42
+ end
43
+
44
+ attr_reader :severity
45
+ attr_reader :message
46
+ attr_reader :data
47
+ attr_reader :exception
48
+
49
+ def message=(arg)
50
+ raise ArgumentError, ":message already specified" if defined?(@message)
51
+
52
+ @message = arg
53
+ end
54
+
55
+ def data=(arg)
56
+ raise ArgumentError, ":data already specified" if defined?(@data)
57
+
58
+ @data = arg
59
+ end
60
+
61
+ def exception=(arg)
62
+ raise ArgumentError, ":exception already specified" if defined?(@exception)
63
+
64
+ @exception = arg
65
+ end
66
+
67
+ def build(*args, &block)
68
+ args.each(&method(:handle_arg))
69
+ if block
70
+ if block.arity.zero?
71
+ Array(block.call).each(&method(:handle_arg))
72
+ else
73
+ block.call(self)
74
+ end
75
+ end
76
+ Entry.with(severity: severity, message: message, data: data, exception: exception)
77
+ end
78
+
79
+ private
80
+
81
+ def handle_arg(arg)
82
+ public_send("#{arg_type(arg)}=", arg)
83
+ end
84
+
85
+ def arg_type(arg)
86
+ case arg
87
+ when String
88
+ :message
89
+ when Hash
90
+ :data
91
+ when Exception
92
+ :exception
93
+ else
94
+ raise ArgumentError, "un-loggable argument: #{arg.inspect}"
95
+ end
96
+ end
97
+
98
+ end
99
+
100
+ end
101
+
102
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "green_log/entry"
4
+ require "green_log/severity"
5
+ require "json"
6
+
7
+ module GreenLog
8
+
9
+ # A JSON-formated log.
10
+ class JsonWriter
11
+
12
+ def initialize(dest)
13
+ @dest = dest
14
+ end
15
+
16
+ attr_reader :dest
17
+
18
+ def <<(entry)
19
+ raise ArgumentError, "GreenLog::Entry expected" unless entry.is_a?(GreenLog::Entry)
20
+
21
+ dest << JSON.dump(entry_details(entry)) + "\n"
22
+ end
23
+
24
+ protected
25
+
26
+ def entry_details(entry)
27
+ {
28
+ "severity" => Severity.name(entry.severity).upcase,
29
+ "message" => entry.message,
30
+ "data" => entry.data,
31
+ "context" => entry.context,
32
+ "exception" => exception_details(entry.exception),
33
+ }.compact
34
+ end
35
+
36
+ def exception_details(exception)
37
+ return nil if exception.nil?
38
+
39
+ {
40
+ "class" => exception.class.name,
41
+ "message" => exception.message,
42
+ "backtrace" => exception.backtrace,
43
+ }
44
+ end
45
+
46
+ end
47
+
48
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "green_log/contextualizer"
4
+ require "green_log/entry"
5
+ require "green_log/severity"
6
+ require "green_log/severity_filter"
7
+ require "green_log/severity_threshold_support"
8
+ require "green_log/simple_writer"
9
+
10
+ module GreenLog
11
+
12
+ # Log entry generator.
13
+ class Logger
14
+
15
+ def initialize(downstream)
16
+ @downstream = downstream
17
+ end
18
+
19
+ attr_reader :downstream
20
+
21
+ include SeverityThresholdSupport
22
+
23
+ # Generate a log entry.
24
+ # Arguments may include:
25
+ # - a message string
26
+ # - arbitrary data
27
+ # - an exception
28
+ def log(severity, *rest, &block)
29
+ severity = Severity.resolve(severity)
30
+ return false if severity < severity_threshold
31
+
32
+ entry = Entry.build(severity, *rest, &block)
33
+ downstream << entry
34
+ true
35
+ end
36
+
37
+ Severity::NAMES.each_with_index do |name, severity|
38
+
39
+ define_method(name.downcase) do |*args, &block|
40
+ log(severity, *args, &block)
41
+ end
42
+
43
+ end
44
+
45
+ # Add a middleware in front of the downstream handler.
46
+ # Return a new Logger with the expanded handler-stack.
47
+ def with_middleware
48
+ self.class.new(yield(downstream))
49
+ end
50
+
51
+ # Add a middleware that adds context.
52
+ # Return a new Logger with the expanded handler-stack.
53
+ def with_context(static_context = nil, &context_generator)
54
+ with_middleware do |current_downstream|
55
+ downstream = current_downstream
56
+ downstream = Contextualizer.new(downstream) { static_context } unless static_context.nil?
57
+ downstream = Contextualizer.new(downstream, &context_generator) unless context_generator.nil?
58
+ downstream
59
+ end
60
+ end
61
+
62
+ # Add a middleware that filters by severity.
63
+ # Return a new Logger with the expanded handler-stack.
64
+ def with_severity_threshold(threshold)
65
+ with_middleware do |downstream|
66
+ SeverityFilter.new(downstream, threshold: threshold)
67
+ end
68
+ end
69
+
70
+ class << self
71
+
72
+ # Build a Logger.
73
+ def build(dest: $stdout, format: SimpleWriter, severity_threshold: nil)
74
+ format = resolve_format(format)
75
+ downstream = format.new(dest)
76
+ downstream = SeverityFilter.new(downstream, threshold: severity_threshold) if severity_threshold
77
+ new(downstream)
78
+ end
79
+
80
+ def resolve_format(format)
81
+ return format if format.is_a?(Class)
82
+
83
+ format = format.to_s if format.is_a?(Symbol)
84
+ GreenLog.const_get("#{format.capitalize}Writer")
85
+ end
86
+
87
+ end
88
+
89
+ end
90
+
91
+ end