betterlog 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,163 @@
1
+ class Log
2
+ class Event
3
+ require 'socket'
4
+
5
+ def self.ify(arg, severity: :debug, notify: nil, rest: {})
6
+ notify ||= rest.delete(:notify)
7
+ if e = arg.ask_and_send(:exception)
8
+ ify(
9
+ {
10
+ error_class: e.class.name,
11
+ message: "#{e.class.name}: #{e.message}",
12
+ backtrace: e.backtrace,
13
+ },
14
+ severity: severity,
15
+ rest: rest,
16
+ notify: notify,
17
+ )
18
+ elsif s = arg.ask_and_send(:to_str)
19
+ new(
20
+ ({ notify: s } if notify).to_h |
21
+ {
22
+ message: s,
23
+ severity: severity,
24
+ } | rest
25
+ )
26
+ elsif h = arg.ask_and_send(:to_hash)
27
+ arg = h | { severity: severity } | rest
28
+ new(
29
+ ({ notify: h[:message] || arg.to_s } if notify).to_h |
30
+ arg
31
+ )
32
+ else
33
+ message = "Logging #{arg.inspect}"
34
+ new(
35
+ ({ notify: message } if notify).to_h |
36
+ {
37
+ message: message,
38
+ severity: severity,
39
+ } | rest
40
+ )
41
+ end
42
+ end
43
+
44
+ def self.parse(json)
45
+ new(JSON.parse(json))
46
+ rescue JSON::ParserError
47
+ end
48
+
49
+ def self.is?(json)
50
+ if json = json.ask_and_send(:to_str)
51
+ data = JSON.parse(json).ask_and_send(:to_hash)
52
+ data&.key?('emitter')
53
+ end
54
+ rescue JSON::ParserError
55
+ false
56
+ end
57
+
58
+ def initialize(data = {})
59
+ data = data.symbolize_keys_recursive | meta
60
+ unless data.key?(:message)
61
+ data[:message] = "a #{data[:type]} type log message of severity #{data[:severity]}"
62
+ end
63
+ data[:severity] =
64
+ begin
65
+ Severity.new((data[:severity] || :debug))
66
+ rescue
67
+ Severity.new(:debug)
68
+ end
69
+ @data = Hash[data.sort_by(&:first)]
70
+ end
71
+
72
+ class BreakCircles
73
+ def initialize
74
+ @seen = {}
75
+ end
76
+
77
+ IN_JSON = Tins::ModuleGroup[
78
+ Float, Integer, NilClass, FalseClass, TrueClass
79
+ ]
80
+
81
+ def perform(object)
82
+ if @seen.key?(object.__id__)
83
+ :circular
84
+ else
85
+ @seen[object.__id__] = true
86
+ case
87
+ when h = object.ask_and_send(:to_hash)
88
+ h.each_with_object({}) { |(k, v), h| h[k.to_s.to_sym] = perform(v) }
89
+ when a = object.ask_and_send(:to_ary)
90
+ a.map { |o| perform(o) }
91
+ when IN_JSON === object
92
+ object
93
+ else
94
+ object.to_s
95
+ end
96
+ end
97
+ end
98
+ end
99
+
100
+ def as_hash(*a)
101
+ BreakCircles.new.perform(@data)
102
+ end
103
+
104
+ def to_json(*a)
105
+ as_hash.to_json(*a)
106
+ end
107
+
108
+ def format(*args)
109
+ Log::EventFormatter.new(self).format(*args)
110
+ end
111
+
112
+ alias to_s format
113
+
114
+ def []=(name, value)
115
+ @data[name.to_sym] = value
116
+ end
117
+
118
+ def [](name)
119
+ @data[name.to_sym]
120
+ end
121
+
122
+ def severity
123
+ @data[:severity]
124
+ end
125
+
126
+ def emitter
127
+ @data[:emitter]
128
+ end
129
+
130
+ def notify?
131
+ @data[:notify]
132
+ end
133
+
134
+ def eql?(other)
135
+ @data.eql? other.instance_variable_get(:@data)
136
+ end
137
+
138
+ alias == eql?
139
+
140
+ def hash
141
+ @data.hash
142
+ end
143
+
144
+ private
145
+
146
+ def meta
147
+ m = {
148
+ timestamp: Time.now.utc.iso8601(3),
149
+ pid: $$,
150
+ program: File.basename($0),
151
+ severity: :debug,
152
+ type: 'rails',
153
+ facility: 'local0',
154
+ host: (Socket.gethostname rescue nil),
155
+ thread_id: Thread.current.object_id
156
+ }
157
+ if defined? GlobalMetadata
158
+ m |= GlobalMetadata.data
159
+ end
160
+ m
161
+ end
162
+ end
163
+ end
@@ -0,0 +1,117 @@
1
+ class Log
2
+ class EventFormatter
3
+ include Term::ANSIColor
4
+
5
+ def initialize(event)
6
+ @event = event
7
+ end
8
+
9
+ def format(pretty: false, color: false, format: :default)
10
+ old_coloring, Term::ANSIColor.coloring = Term::ANSIColor.coloring?, color
11
+ f = cc.log.formats[format] and format = f
12
+ case pretty
13
+ when :format
14
+ format_pattern(format: format)
15
+ else
16
+ @event.to_json
17
+ end
18
+ ensure
19
+ Term::ANSIColor.coloring = old_coloring
20
+ end
21
+
22
+ private
23
+
24
+ def colorize(key, value, string = key)
25
+ case style = cc.log.styles[key]
26
+ when nil, String, Array
27
+ apply_style(style, string)
28
+ when ComplexConfig::Settings
29
+ apply_style(style[value], string)
30
+ end
31
+ end
32
+
33
+ def apply_style(style, string)
34
+ style.nil? and return string + Term::ANSIColor.reset
35
+ string = Term::ANSIColor.uncolor(string)
36
+ if style.respond_to?(:each)
37
+ style.
38
+ each.
39
+ map { |s| -> v { __send__(:color, s, v) } }.
40
+ inject(string) { |v, s| s.(v) }
41
+ else
42
+ __send__(:color, style, string)
43
+ end
44
+ end
45
+
46
+ def format_pattern(format:)
47
+ format.
48
+ gsub('\n', "\n").
49
+ gsub('\t', "\t").
50
+ gsub(/\{(-)?(%[^%]+%)?([^}]+)\}/) {
51
+ invisible = $1.full?
52
+ directive = $2
53
+ key = $3
54
+ if value = @event[key]
55
+ formatted_value =
56
+ if directive
57
+ case directive
58
+ when /\A%O%/
59
+ format_object(value)
60
+ when /\A%([ulif])?t%/
61
+ flag = $1
62
+ t = case
63
+ when v = value.ask_and_send(:to_str)
64
+ Time.parse(v)
65
+ when v = value.ask_and_send(:to_time)
66
+ v
67
+ else
68
+ Time.at(0)
69
+ end
70
+ case flag
71
+ when ?u then t.utc.iso8601(3)
72
+ when ?l then t.localtime.iso8601(3)
73
+ when ?i then t.to_i.to_s
74
+ when ?f then t.to_f.to_s
75
+ else t.utc.iso8601(3)
76
+ end
77
+ else
78
+ begin
79
+ directive[0..-2] % value
80
+ rescue ArgumentError
81
+ value.to_s
82
+ end
83
+ end
84
+ else
85
+ value.to_s
86
+ end
87
+ colorize(key, value, formatted_value)
88
+ else
89
+ unless invisible
90
+ "{#{key}}"
91
+ end
92
+ end
93
+ }
94
+ end
95
+
96
+ def format_object(object, depth: 0, nl: ?\n)
97
+ case
98
+ when a = object.ask_and_send(:to_ary)
99
+ result = ''
100
+ depth += 2
101
+ for v in a
102
+ result << "\n#{' ' * depth}- #{format_object(v, depth: depth)}"
103
+ end
104
+ result
105
+ when h = object.ask_and_send(:to_hash)
106
+ result = ''
107
+ depth += 2
108
+ for k, v in h
109
+ result << "\n#{' ' * depth}#{k}: #{format_object(v, depth: depth)}"
110
+ end
111
+ result
112
+ else
113
+ object.to_s
114
+ end
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,49 @@
1
+ class Log
2
+ class Severity
3
+ include Comparable
4
+
5
+ def initialize(name)
6
+ @name = name.to_s.downcase.to_sym
7
+ begin
8
+ @level = ActiveSupport::Logger::Severity.const_get(@name.upcase)
9
+ rescue NameError
10
+ @name = :UNKNOWN
11
+ @level = ActiveSupport::Logger::Severity::UNKNOWN
12
+ end
13
+ end
14
+
15
+ def self.all
16
+ ActiveSupport::Logger::Severity.constants.map { |c| new(c) }
17
+ end
18
+
19
+ def to_i
20
+ @level
21
+ end
22
+
23
+ def to_s
24
+ @name.to_s.upcase
25
+ end
26
+
27
+ def to_sym
28
+ @name
29
+ end
30
+
31
+ def as_json(*)
32
+ to_sym
33
+ end
34
+
35
+ def <=>(other)
36
+ to_i <=> other.to_i
37
+ end
38
+
39
+ def eql?(other)
40
+ to_sym == other.to_sym
41
+ end
42
+
43
+ alias == eql?
44
+
45
+ def hash
46
+ @name.hash
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,181 @@
1
+ require 'tins'
2
+ require 'tins/xt'
3
+ require 'betterlog/log/event'
4
+ require 'betterlog/log/event_formatter'
5
+ require 'betterlog/log/severity'
6
+
7
+ class Log
8
+ include Tins::SexySingleton
9
+
10
+ def logger
11
+ Rails.logger
12
+ end
13
+
14
+ # Logs a message on severity info.
15
+ #
16
+ # @param object this object is logged
17
+ # @param **rest additional data is logged as well.
18
+ # @return [ Log ] this object itself.
19
+ def info(object, **rest)
20
+ protect do
21
+ emit Log::Event.ify(object, severity: __method__, rest: rest)
22
+ end
23
+ end
24
+
25
+ # Logs a message on severity warn.
26
+ #
27
+ # @param object this object is logged
28
+ # @param **rest additional data is logged as well.
29
+ # @return [ Log ] this object itself.
30
+ def warn(object, **rest)
31
+ protect do
32
+ emit Log::Event.ify(object, severity: __method__, rest: rest)
33
+ end
34
+ end
35
+
36
+ # Logs a message on severity debug.
37
+ #
38
+ # @param object this object is logged
39
+ # @param **rest additional data is logged as well.
40
+ # @return [ Log ] this object itself.
41
+ def debug(object, **rest)
42
+ protect do
43
+ emit Log::Event.ify(object, severity: __method__, rest: rest)
44
+ end
45
+ end
46
+
47
+ # Logs a message on severity error.
48
+ #
49
+ # @param object this object is logged
50
+ # @param **rest additional data is logged as well.
51
+ # @return [ Log ] this object itself.
52
+ def error(object, **rest)
53
+ protect do
54
+ emit Log::Event.ify(object, severity: __method__, rest: rest)
55
+ end
56
+ end
57
+
58
+ # Logs a message on severity fatal.
59
+ #
60
+ # @param object this object is logged
61
+ # @param **rest additional data is logged as well.
62
+ # @return [ Log ] this object itself.
63
+ def fatal(object, **rest)
64
+ protect do
65
+ emit Log::Event.ify(object, severity: __method__, rest: rest)
66
+ end
67
+ end
68
+
69
+ # Logs a message on severity debug, by default, this can be changed by
70
+ # passing the severity: keyword.
71
+ #
72
+ # @param object this object is logged
73
+ # @param **rest additional data is logged as well.
74
+ # @return [ Log ] this object itself.
75
+ def output(object, **rest)
76
+ protect do
77
+ emit Log::Event.ify(object, severity: rest[:severity], rest: rest)
78
+ end
79
+ end
80
+
81
+ # Logs a metric on severity debug, by default, this can be changed by passing
82
+ # the severity: keyword.
83
+ #
84
+ # @param metric the name of the recorded metric.
85
+ # @param type of the recorded metric.
86
+ # @param value of the recorded metric.
87
+ # @param **rest additional rest is logged as well.
88
+ # @return [ Log ] this object itself.
89
+ def metric(metric:, type:, value:, **rest)
90
+ protect do
91
+ event = build_metric(metric: metric, type: type, value: value, **rest)
92
+ emit event
93
+ end
94
+ end
95
+
96
+ # Logs a time measure on severity debug, by default, this can be changed by
97
+ # passing the severity: keyword.
98
+ #
99
+ # If an error occurs during measurement details about it are added to the
100
+ # metric event.
101
+ #
102
+ # @param metric the name of the recorded metric.
103
+ # @param **rest additional rest is logged as well.
104
+ # @param block the block around which the measure is teaken.
105
+ # @return [ Log ] this object itself.
106
+ def measure(metric:, **rest, &block)
107
+ raise ArgumentError, 'must be called with a block' unless block_given?
108
+ time_block { yield }
109
+ rescue => error
110
+ e = Log::Event.ify(error)
111
+ rest |= e.as_hash.subhash(:error_class, :backtrace, :message)
112
+ rest[:message] = "#{rest[:message]} while measuring metric #{metric}"
113
+ raise error
114
+ ensure
115
+ protect do
116
+ event = build_metric(metric: metric, type: 'seconds', value: timed_duration, **rest)
117
+ emit event
118
+ end
119
+ end
120
+
121
+ private
122
+
123
+ def protect
124
+ yield
125
+ rescue => e
126
+ begin
127
+ # Try logging e once by ourselves
128
+ emit Log::Event.ify(e, severity: :fatal)
129
+ rescue
130
+ # Ok, I give up let's use logger directly instead
131
+ logger.fatal(
132
+ "Crashed during logging with #{e.class}: #{e.message}):\n"\
133
+ "#{e.backtrace * ?\n}"
134
+ )
135
+ end
136
+ self
137
+ end
138
+
139
+ def build_metric(metric:, type:, value:, **rest)
140
+ severity = rest.fetch(:severity, :debug)
141
+ rest |= {
142
+ message: "a metric #{metric} of type #{type}",
143
+ }
144
+ Log::Event.ify(
145
+ {
146
+ metric: metric,
147
+ type: type,
148
+ value: value,
149
+ } | rest,
150
+ severity: severity
151
+ )
152
+ end
153
+
154
+ def emit(event)
155
+ if l = caller_locations.reverse_each.each_cons(3).find { |c, n1, n2|
156
+ n2.absolute_path =~ /app\/lib\/log\.rb/ and break c
157
+ }
158
+ then
159
+ event[:location] = [ l.absolute_path, l.lineno ] * ?:
160
+ end
161
+ event[:emitter] = self.class.name.downcase
162
+ if event.notify?
163
+ notify(event)
164
+ end
165
+ logger.send(event.severity.to_sym, event.to_json)
166
+ self
167
+ end
168
+
169
+ def notify(event)
170
+ Honeybadger.notify(event.notify?, event.as_hash)
171
+ end
172
+
173
+ thread_local :timed_duration
174
+
175
+ def time_block
176
+ s = Time.now
177
+ yield
178
+ ensure
179
+ self.timed_duration = Time.now - s
180
+ end
181
+ end
@@ -0,0 +1,47 @@
1
+ class LogEventFormatter < ActiveSupport::Logger::Formatter
2
+ include ActiveSupport::TaggedLogging::Formatter
3
+
4
+ def emitter
5
+ 'legacy'
6
+ end
7
+
8
+ def call(severity, timestamp, program, message)
9
+ super
10
+ message = message.to_s
11
+ if cc.log.legacy_supported
12
+ if message.blank?
13
+ return ''
14
+ elsif !Log::Event.is?(message)
15
+ m = message.sub(/\s+$/, '')
16
+ timestamp = timestamp.utc.iso8601(3)
17
+ event = Log::Event.new(
18
+ emitter: emitter,
19
+ timestamp: timestamp,
20
+ message: m,
21
+ severity: severity.to_s.downcase,
22
+ # tags: current_tags,
23
+ )
24
+ if backtrace = m.grep(/^\s*([^:]+):(\d+)/)
25
+ if backtrace.size > 1
26
+ event[:backtrace] = backtrace.map(&:chomp)
27
+ event[:message] = 'a logged backtrace'
28
+ end
29
+ end
30
+ if l = caller_locations.reverse_each.each_cons(2).find { |c, n|
31
+ n.absolute_path =~ /\/lib\/ruby\/.*?\/logger\.rb/ and break c
32
+ }
33
+ then
34
+ event[:location] = [ l.absolute_path, l.lineno ] * ?:
35
+ end
36
+ program and event[:program] = program
37
+ message = event.to_json
38
+ end
39
+ end
40
+ rescue => e
41
+ Rails.logger.error(e)
42
+ ensure
43
+ # Do not "message << ?\n" - A frozn string may be passed in
44
+ message.end_with?(?\n) or message = "#{message}\n"
45
+ return message
46
+ end
47
+ end
@@ -0,0 +1,8 @@
1
+ module Betterlog
2
+ # Betterlog version
3
+ VERSION = '0.1.0'
4
+ VERSION_ARRAY = VERSION.split('.').map(&:to_i) # :nodoc:
5
+ VERSION_MAJOR = VERSION_ARRAY[0] # :nodoc:
6
+ VERSION_MINOR = VERSION_ARRAY[1] # :nodoc:
7
+ VERSION_BUILD = VERSION_ARRAY[2] # :nodoc:
8
+ end
data/lib/betterlog.rb ADDED
@@ -0,0 +1,14 @@
1
+ require 'tins/xt'
2
+ require 'json'
3
+ require 'logger'
4
+ require 'time'
5
+ require 'tins'
6
+ require 'rails'
7
+
8
+ require 'active_support'
9
+ require 'term/ansicolor'
10
+
11
+ require 'betterlog/global_metadata'
12
+ require 'betterlog/betterlog_railtie'
13
+ require 'betterlog/log'
14
+ require 'betterlog/log_event_formatter'
data/log.yml ADDED
@@ -0,0 +1,77 @@
1
+ default: &default
2
+ styles:
3
+ 'timestamp': [ yellow, bold ]
4
+ 'file': [ blue, bold ]
5
+ severity:
6
+ debug: green
7
+ info: green
8
+ warn: yellow
9
+ error: red
10
+ fatal: [ red, blink ]
11
+ unknown: red
12
+ formats:
13
+ default: &default_format >
14
+ {%lt%timestamp} {%5s%severity}
15
+ "{%0.<%= ENV.fetch('COLUMNS', 80).to_i / 2 %>%message}"
16
+ {location}
17
+ {file}{-%O%backtrace}{-%O%meta}
18
+ d: *default_format
19
+ long: &long_format |
20
+ timestamp: {%lt%timestamp}
21
+ severity: {severity}
22
+ message: "{message}"
23
+ metric: {metric} {value} {type}
24
+ error_class: {error_class}
25
+ backtrace: {%O%backtrace}
26
+ location: {location}
27
+ file: {file}
28
+ meta: {%O%meta}\n
29
+ l: *long_format
30
+ legacy: >
31
+ {%0.1s%severity} [{%lt%timestamp} #{%d%pid}] {%5s%severity} --
32
+ {program}: {message}
33
+ metric: >
34
+ {%ft%timestamp} {metric} {value} {type}
35
+
36
+ development:
37
+ <<: *default
38
+ config_files:
39
+ rails:
40
+ - log/development.log
41
+ test:
42
+ - log/test.log
43
+ redis:
44
+ - /usr/local/var/log/redis.log
45
+ elasticsearch:
46
+ - /usr/local/var/log/elasticsearch@1.7.log
47
+ nginx:
48
+ - /usr/local/var/log/nginx/access.log
49
+ - /usr/local/var/log/nginx/error.log
50
+ legacy_supported: <%= ENV['LOG_LEGACY_SUPPORTED'].to_i == 1 %>
51
+
52
+ test:
53
+ <<: *default
54
+ config_files:
55
+ test:
56
+ - log/test.log
57
+ legacy_supported: <%= ENV['LOG_LEGACY_SUPPORTED'].to_i == 1 %>
58
+
59
+ production:
60
+ <<: *default
61
+ config_files:
62
+ rails:
63
+ - /var/apps/betterplace/current/log/production.log
64
+ cron:
65
+ - /var/apps/betterplace/current/log/cron.log
66
+ nginx:
67
+ - /var/apps/betterplace/current/log/nginx.assets.access.log
68
+ - /var/apps/betterplace/current/log/nginx.assets.error.log
69
+ - /var/apps/betterplace/current/log/nginx.betterplace.access.log
70
+ - /var/apps/betterplace/current/log/nginx.betterplace.error.log
71
+ - /var/apps/betterplace/current/log/nginx.default.access.log
72
+ - /var/apps/betterplace/current/log/nginx.default.error.log
73
+ memosig:
74
+ - /var/log/memosig/current
75
+ unicorn:
76
+ - /var/log/unicorn/current
77
+ legacy_supported: yes