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.
- checksums.yaml +7 -0
- data/README.md +194 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/doc/adr/0001-record-architecture-decisions.md +19 -0
- data/doc/adr/0002-avoid-global-configuration.md +35 -0
- data/doc/adr/0003-decouple-generation-and-handling.md +30 -0
- data/doc/adr/0004-use-stacked-handlers-to-solve-many-problems.md +51 -0
- data/doc/adr/0005-restrict-data-types-allowable-in-log-entries.md +27 -0
- data/doc/adr/0006-use-lock-free-io.md +29 -0
- data/examples/multi-threaded +21 -0
- data/examples/try-it +21 -0
- data/green_log.gemspec +35 -0
- data/lib/green_log.rb +6 -0
- data/lib/green_log/classic_logger.rb +80 -0
- data/lib/green_log/contextualizer.rb +26 -0
- data/lib/green_log/core_refinements.rb +73 -0
- data/lib/green_log/entry.rb +102 -0
- data/lib/green_log/json_writer.rb +48 -0
- data/lib/green_log/logger.rb +91 -0
- data/lib/green_log/rack/request_logging.rb +86 -0
- data/lib/green_log/severity.rb +49 -0
- data/lib/green_log/severity_filter.rb +26 -0
- data/lib/green_log/severity_threshold_support.rb +18 -0
- data/lib/green_log/simple_writer.rb +85 -0
- data/lib/green_log/version.rb +7 -0
- metadata +166 -0
data/examples/try-it
ADDED
|
@@ -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
|
data/green_log.gemspec
ADDED
|
@@ -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
|
data/lib/green_log.rb
ADDED
|
@@ -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
|