mimi-logger 0.1.2 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/README.md +83 -1
- data/lib/mimi/logger/version.rb +1 -1
- data/lib/mimi/logger.rb +163 -7
- metadata +3 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 1dac6f3cbaeef0582579cb7f695e92896676d9267c286e7d82d95d7d72312219
|
4
|
+
data.tar.gz: 3e9866931dff24f400dca04da3e1e41d59139f33860c621d2e7136656f80f87d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0c02fa6f4e89c807b5b6284aec1e304939cf3aec08820c73db08380b81a0b0f071e85e99c55810bf8e0b584a5497f227748311dd44b7d4e9b087b649970a2b2a
|
7
|
+
data.tar.gz: 62cdc14d5fcbfe555e2fe45af17db5c004d051767445802dbb35fa3525825f4de514afad51da9fd700f110ae4c1ad70680344da47641b3bba50250a7c2dfdf9e
|
data/README.md
CHANGED
@@ -25,11 +25,93 @@ Or install it yourself as:
|
|
25
25
|
```ruby
|
26
26
|
require 'mimi/logger'
|
27
27
|
|
28
|
-
logger = Mimi::Logger.new
|
28
|
+
logger = Mimi::Logger.new format: :string
|
29
29
|
|
30
30
|
logger.info 'I am a banana!' # outputs "I, I am a banana!" to STDOUT
|
31
31
|
```
|
32
32
|
|
33
|
+
### Logging format
|
34
|
+
|
35
|
+
Since v0.2.0 the default format for logging is JSON:
|
36
|
+
|
37
|
+
```ruby
|
38
|
+
require 'mimi/logger'
|
39
|
+
|
40
|
+
logger = Mimi::Logger.new
|
41
|
+
|
42
|
+
logger.info 'I am a banana' # => '{"s":"I","m":"I am a banana","c":"60eecc2e764fe2f6"}'
|
43
|
+
```
|
44
|
+
|
45
|
+
The following properties of a serialized JSON object are reserved:
|
46
|
+
|
47
|
+
name | description
|
48
|
+
-----|------------
|
49
|
+
s | severity, one of D,I,W,E,F
|
50
|
+
m | message
|
51
|
+
c | context ID, 8 bytes hex encoded
|
52
|
+
|
53
|
+
Additional properties may be provided by the caller and they will be included in the logged JSON:
|
54
|
+
|
55
|
+
```ruby
|
56
|
+
require 'mimi/logger'
|
57
|
+
|
58
|
+
logger = Mimi::Logger.new
|
59
|
+
|
60
|
+
t_start = Time.now
|
61
|
+
... # work, work
|
62
|
+
logger.info 'Jobs done', t: Time.now - t_start
|
63
|
+
# => '{"s":"I","m":"Jobs done","c":"60eecc2e764fe2f6", "t": 13.794034499}'
|
64
|
+
```
|
65
|
+
|
66
|
+
### Logging context
|
67
|
+
|
68
|
+
Logging context refers to a set of instructions that are somehow logically grouped. For example,
|
69
|
+
the whole logic processing an incoming web request can be seen as operating within a single context
|
70
|
+
of this particular web request. In a distributed or a multithreaded application it would be beneficial
|
71
|
+
to identify logged messages produced within same context, otherwise the messages related to different
|
72
|
+
web requests will be interleaved and understanding the flow, the cause and effect would be very difficult.
|
73
|
+
|
74
|
+
To solve this problem, Mimi::Logger allows for setting an arbitrary (or random) context ID, which is local
|
75
|
+
to the current thread, and which is included in every logged message.
|
76
|
+
|
77
|
+
A new context may be initiated by `.new_context_id!`:
|
78
|
+
|
79
|
+
```ruby
|
80
|
+
require 'mimi/logger'
|
81
|
+
|
82
|
+
logger = Mimi::Logger.new
|
83
|
+
|
84
|
+
logger.info 'I am a banana!'
|
85
|
+
logger.new_context_id!
|
86
|
+
logger.info 'I am a banana!' # this is not the same banana, it's from a different context
|
87
|
+
```
|
88
|
+
|
89
|
+
Or it can be set to an explicit value:
|
90
|
+
|
91
|
+
```ruby
|
92
|
+
require 'mimi/logger'
|
93
|
+
|
94
|
+
logger = Mimi::Logger.new
|
95
|
+
|
96
|
+
logger.info 'I am a banana!'
|
97
|
+
logger.context_id = 'different-context!'
|
98
|
+
logger.info 'I am a banana!' # this is not the same banana, it's from a 'different-context!'
|
99
|
+
|
100
|
+
```
|
101
|
+
|
102
|
+
#### Context ID in a multithreaded application
|
103
|
+
|
104
|
+
The context ID is local to a current thread, so it's safe to start or assign a different context ID
|
105
|
+
in other threads of the application.
|
106
|
+
|
107
|
+
#### Context ID in a distributed application
|
108
|
+
|
109
|
+
If you have a distributed multicomponent application (e.g. microservice architecture), context ID
|
110
|
+
may help to track requests between multiple parties. In order to achieve it, you need to generate a
|
111
|
+
new context ID in the beginning of your processing and pass it along with all the requests/messages to
|
112
|
+
other components of the system. Upon receiving such a request, another application sets its local context ID
|
113
|
+
to the received value and continues.
|
114
|
+
|
33
115
|
## Contributing
|
34
116
|
|
35
117
|
Bug reports and pull requests are welcome on GitHub at https://github.com/kukushkin/mimi-logger. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
|
data/lib/mimi/logger/version.rb
CHANGED
data/lib/mimi/logger.rb
CHANGED
@@ -1,5 +1,8 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'forwardable'
|
2
4
|
require 'logger'
|
5
|
+
require 'json'
|
3
6
|
require 'mimi/core'
|
4
7
|
|
5
8
|
module Mimi
|
@@ -7,17 +10,21 @@ module Mimi
|
|
7
10
|
# Mimi::Logger is a preconfigured logger which outputs log messages to STDOUT.
|
8
11
|
#
|
9
12
|
class Logger
|
13
|
+
CONTEXT_ID_SIZE = 8 # bytes
|
14
|
+
CONTEXT_ID_THREAD_VARIABLE = 'mimi_logger_context_id'
|
15
|
+
|
10
16
|
extend Forwardable
|
11
17
|
include Mimi::Core::Module
|
12
18
|
|
13
19
|
attr_reader :logger_instance
|
14
20
|
delegate [
|
15
|
-
:debug, :info, :warn, :error, :fatal, :unknown,
|
16
21
|
:debug?, :info?, :warn?, :error?, :fatal?,
|
17
22
|
:<<, :add, :log
|
18
23
|
] => :logger_instance
|
19
24
|
|
20
25
|
default_options(
|
26
|
+
format: 'json', # 'string' or 'json'
|
27
|
+
log_context: true,
|
21
28
|
level: 'info',
|
22
29
|
cr_character: '↲' # alternative CR: ↵ ↲ ⏎
|
23
30
|
)
|
@@ -36,7 +43,7 @@ module Mimi
|
|
36
43
|
# logger.debug 'blabla' # => "D, blabla"
|
37
44
|
#
|
38
45
|
def initialize(*args)
|
39
|
-
io = args.shift if args.first.is_a?(IO)
|
46
|
+
io = args.shift if args.first.is_a?(IO) || args.first.is_a?(StringIO)
|
40
47
|
io ||= STDOUT
|
41
48
|
opts = args.shift if args.first.is_a?(Hash)
|
42
49
|
opts ||= {}
|
@@ -51,7 +58,7 @@ module Mimi
|
|
51
58
|
|
52
59
|
# Returns the current log level
|
53
60
|
#
|
54
|
-
# @return [
|
61
|
+
# @return [Integer]
|
55
62
|
#
|
56
63
|
def level
|
57
64
|
logger_instance.level
|
@@ -60,18 +67,41 @@ module Mimi
|
|
60
67
|
# Sets the log level.
|
61
68
|
# Allows setting the log level from a String or a Symbol, in addition to the standard ::Logger::INFO etc.
|
62
69
|
#
|
63
|
-
# @param [String,Symbol,
|
70
|
+
# @param [String,Symbol,Integer] value
|
64
71
|
#
|
65
72
|
def level=(value)
|
66
73
|
logger_instance.level = self.class.level_from_any(value)
|
67
74
|
end
|
68
75
|
|
76
|
+
# Logs a new message at the corresponding logging level
|
77
|
+
#
|
78
|
+
#
|
79
|
+
#
|
80
|
+
def debug(*args, &block)
|
81
|
+
logger_instance.debug(args, &block)
|
82
|
+
end
|
83
|
+
def info(*args, &block)
|
84
|
+
logger_instance.info(args, &block)
|
85
|
+
end
|
86
|
+
def warn(*args, &block)
|
87
|
+
logger_instance.warn(args, &block)
|
88
|
+
end
|
89
|
+
def error(*args, &block)
|
90
|
+
logger_instance.error(args, &block)
|
91
|
+
end
|
92
|
+
def fatal(*args, &block)
|
93
|
+
logger_instance.fatal(args, &block)
|
94
|
+
end
|
95
|
+
def unknown(*args, &block)
|
96
|
+
logger_instance.unknown(args, &block)
|
97
|
+
end
|
98
|
+
|
69
99
|
# Returns the log level inferred from value
|
70
100
|
#
|
71
|
-
# @param value [String,Symbol,
|
101
|
+
# @param value [String,Symbol,Integer]
|
72
102
|
#
|
73
103
|
def self.level_from_any(value)
|
74
|
-
return value if value.is_a?(
|
104
|
+
return value if value.is_a?(Integer)
|
75
105
|
case value.to_s.downcase.to_sym
|
76
106
|
when :debug
|
77
107
|
::Logger::DEBUG
|
@@ -90,12 +120,138 @@ module Mimi
|
|
90
120
|
end
|
91
121
|
end
|
92
122
|
|
123
|
+
# Returns formatter Proc object depending on configured format
|
124
|
+
#
|
125
|
+
# @return [Proc]
|
126
|
+
#
|
93
127
|
def self.formatter
|
128
|
+
case module_options[:format].to_s
|
129
|
+
when 'json'
|
130
|
+
formatter_json
|
131
|
+
when 'string'
|
132
|
+
formatter_string
|
133
|
+
else
|
134
|
+
raise "Invalid format specified for Mimi::Logger: '#{module_options[:format]}'"
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
# Returns formatter for 'json' format
|
139
|
+
#
|
140
|
+
# @return [Proc]
|
141
|
+
#
|
142
|
+
def self.formatter_json
|
94
143
|
proc do |severity, _datetime, _progname, message|
|
95
|
-
|
144
|
+
h = formatter_message_args_to_hash(message)
|
145
|
+
h[:c] = context_id if module_options[:log_context]
|
146
|
+
h[:s] = severity.to_s[0]
|
147
|
+
h.to_json
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
# Returns formatter for 'string' format
|
152
|
+
#
|
153
|
+
# @return [Proc]
|
154
|
+
#
|
155
|
+
def self.formatter_string
|
156
|
+
proc do |severity, _datetime, _progname, message|
|
157
|
+
h = formatter_message_args_to_hash(message)
|
158
|
+
parts = []
|
159
|
+
parts << severity.to_s[0]
|
160
|
+
parts << context_id if module_options[:log_context]
|
161
|
+
parts << h[:m].to_s.tr("\n", module_options[:cr_character])
|
162
|
+
parts << '...' unless h.except(:m).empty?
|
163
|
+
parts.join(', ')
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
# Converts logger methods arguments passed in various forms to a message hash.
|
168
|
+
#
|
169
|
+
# Arguments to a logger may be passed in 6 different ways:
|
170
|
+
# @example
|
171
|
+
# logger.info('String')
|
172
|
+
# logger.info('String', param1: '..and a Hash')
|
173
|
+
# logger.info(m: 'Just a Hash', param1: 'with optional data')
|
174
|
+
#
|
175
|
+
# # and the same 3 ways in a block form
|
176
|
+
# logger.info { ... }
|
177
|
+
#
|
178
|
+
# This helper method converts all possible variations into one Hash, where key m: refers to the
|
179
|
+
# message and the rest are optional parameters passed in a Hash argument.
|
180
|
+
#
|
181
|
+
# @return [Hash]
|
182
|
+
#
|
183
|
+
def self.formatter_message_args_to_hash(message_args)
|
184
|
+
message_args = message_args.is_a?(String) || message_args.is_a?(Hash) ? [message_args] : message_args
|
185
|
+
if !message_args.is_a?(Array) || message_args.size > 2
|
186
|
+
raise ArgumentError, "Mimi::Logger arguments expected to be Array of up to 2 elements: #{message_args.inspect}"
|
96
187
|
end
|
188
|
+
|
189
|
+
h = {}
|
190
|
+
arg1 = message_args.shift
|
191
|
+
arg2 = message_args.shift
|
192
|
+
|
193
|
+
if arg1.is_a?(String)
|
194
|
+
if arg2 && !arg2.is_a?(Hash)
|
195
|
+
raise ArgumentError, 'Mimi::Logger arguments are expected to be one of (<String>, <Hash>, [<String>, <Hash>])'
|
196
|
+
end
|
197
|
+
h = arg2.dup || {}
|
198
|
+
h[:m] = arg1
|
199
|
+
elsif arg1.is_a?(Hash)
|
200
|
+
if arg2
|
201
|
+
raise ArgumentError, 'Mimi::Logger arguments are expected to be one of (<String>, <Hash>, [<String>, <Hash>])'
|
202
|
+
end
|
203
|
+
h = arg1.dup
|
204
|
+
else
|
205
|
+
raise ArgumentError, 'Mimi::Logger arguments are expected to be one of (<String>, <Hash>, [<String>, <Hash>])'
|
206
|
+
end
|
207
|
+
h
|
208
|
+
end
|
209
|
+
|
210
|
+
# Returns current context ID if set, otherwise generates and sets a new context ID
|
211
|
+
# and returns it.
|
212
|
+
#
|
213
|
+
# Context ID is local to the current thread. It identifies a group of instructions
|
214
|
+
# happening within same logical context, as a single operation. For example, processing
|
215
|
+
# an incoming request may be seen as a single context.
|
216
|
+
#
|
217
|
+
# Context ID is logged with every message
|
218
|
+
#
|
219
|
+
# @return [String] a hex encoded context ID
|
220
|
+
#
|
221
|
+
def self.context_id
|
222
|
+
Thread.current[CONTEXT_ID_THREAD_VARIABLE] || new_context_id!
|
97
223
|
end
|
98
224
|
|
225
|
+
def context_id
|
226
|
+
self.class.context_id
|
227
|
+
end
|
228
|
+
|
229
|
+
# Sets the new context ID to the given value
|
230
|
+
#
|
231
|
+
# @param id [String]
|
232
|
+
#
|
233
|
+
def self.context_id=(id)
|
234
|
+
Thread.current[CONTEXT_ID_THREAD_VARIABLE] = id
|
235
|
+
end
|
236
|
+
|
237
|
+
def context_id=(id)
|
238
|
+
self.class.context_id = id
|
239
|
+
end
|
240
|
+
|
241
|
+
# Generates a new random context ID and sets it as current
|
242
|
+
#
|
243
|
+
# @return [String] a new context ID
|
244
|
+
#
|
245
|
+
def self.new_context_id!
|
246
|
+
self.context_id = SecureRandom.hex(CONTEXT_ID_SIZE)
|
247
|
+
end
|
248
|
+
|
249
|
+
def new_context_id!
|
250
|
+
self.class.new_context_id!
|
251
|
+
end
|
252
|
+
|
253
|
+
# private-ish
|
254
|
+
|
99
255
|
def self.module_manifest
|
100
256
|
{
|
101
257
|
log_level: {
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: mimi-logger
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Alex Kukushkin
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2018-04-18 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: mimi-core
|
@@ -123,7 +123,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
123
123
|
version: '0'
|
124
124
|
requirements: []
|
125
125
|
rubyforge_project:
|
126
|
-
rubygems_version: 2.
|
126
|
+
rubygems_version: 2.7.6
|
127
127
|
signing_key:
|
128
128
|
specification_version: 4
|
129
129
|
summary: A pre-configured logger for microservice applications
|