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
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: a3c301ce67ee757ddf2093b93fc973ff46ad005f2ea7e8da3e693ca01a6c7444
|
|
4
|
+
data.tar.gz: 0ec5551c9554ec465acfd815f4a95514f8dbf48529427a0187d51b02657e2671
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 889da34f831f8d7b95bb79343b6905f88d0ffde04a308773ec6775ed70f2b7ffd8668f7eb9c0c8b6ec5fc4d7a2ffbbc6ff59ccecbbdac34fec6ac598c35bba0b
|
|
7
|
+
data.tar.gz: f72a8e18d791b8d57a6a413d40c870c7dfb3b34414dc6e00862972acf927ef34d3520676cc973f2fdb7c29f0d92b9d15a69fed230d257b8f19c069b9f8523e7e
|
data/README.md
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
# GreenLog
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+
|
|
5
|
+
GreenLog is a logging library for Ruby applications. It:
|
|
6
|
+
|
|
7
|
+
- focuses on [structured logging](https://www.thoughtworks.com/radar/techniques/structured-logging) - treating log entries as data
|
|
8
|
+
- is optimised for use in modern "cloud-native" applications
|
|
9
|
+
- can be used in place of Ruby's stdlib `Logger`
|
|
10
|
+
|
|
11
|
+
## Design approach
|
|
12
|
+
|
|
13
|
+
GreenLog:
|
|
14
|
+
|
|
15
|
+
- [avoids global state](doc/adr/0002-avoid-global-configuration.md)
|
|
16
|
+
- avoids mutable objects
|
|
17
|
+
- explicitly [decouples log entry generation and handling](doc/adr/0003-decouple-generation-and-handling.md)
|
|
18
|
+
- uses [an approach similar to Rack middleware](doc/adr/0004-use-stacked-handlers-to-solve-many-problems.md) for flexible log processing
|
|
19
|
+
- uses [lock-free IO](doc/adr/0006-use-lock-free-io.md) for performance
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
Add this line to your application's Gemfile:
|
|
24
|
+
|
|
25
|
+
```ruby
|
|
26
|
+
gem 'green_log'
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
And then execute:
|
|
30
|
+
|
|
31
|
+
$ bundle
|
|
32
|
+
|
|
33
|
+
## Usage
|
|
34
|
+
|
|
35
|
+
### tl;dr
|
|
36
|
+
|
|
37
|
+
```ruby
|
|
38
|
+
require 'green_log'
|
|
39
|
+
|
|
40
|
+
logger = GreenLog::Logger.build
|
|
41
|
+
|
|
42
|
+
logger.info("Stuff happened")
|
|
43
|
+
# outputs: I -- Stuff happened
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Basic logging
|
|
47
|
+
|
|
48
|
+
GreenLog implements all the expected logging shortcuts:
|
|
49
|
+
|
|
50
|
+
```ruby
|
|
51
|
+
logger.debug("Nitty gritty detail")
|
|
52
|
+
# outputs: D -- Nitty gritty detail
|
|
53
|
+
logger.info("Huh, interesting.")
|
|
54
|
+
# outputs: I -- Huh, interesting.
|
|
55
|
+
logger.warn("Careful now.")
|
|
56
|
+
# outputs: W -- Careful now.
|
|
57
|
+
logger.error("Oh, that's really not good!")
|
|
58
|
+
# outputs: E -- Oh, that's really not good!
|
|
59
|
+
logger.fatal("Byeeee ...")
|
|
60
|
+
# outputs: F -- Byeeee ...
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Adding context
|
|
64
|
+
|
|
65
|
+
`Logger#with_context` adds detail about the _source_ of log messages.
|
|
66
|
+
|
|
67
|
+
```ruby
|
|
68
|
+
logger = GreenLog::Logger.build.with_context(pid: Process.pid, thread: Thread.current.object_id)
|
|
69
|
+
logger.info("Hello")
|
|
70
|
+
# outputs: I [pid=13545 thread=70260187418160] -- Hello
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
It can be chained to inject additional context:
|
|
74
|
+
|
|
75
|
+
```ruby
|
|
76
|
+
logger.with_context(request: 16273).info("Handled")
|
|
77
|
+
# outputs: I [pid=13545 thread=70260187418160 request=16273] -- Handled
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Context can also be calculated dynamically, using a block:
|
|
81
|
+
|
|
82
|
+
```ruby
|
|
83
|
+
logger = GreenLog::Logger.build.with_context do
|
|
84
|
+
{
|
|
85
|
+
request_id: Thread.current[:request_id]
|
|
86
|
+
}
|
|
87
|
+
end
|
|
88
|
+
# outputs: I [pid=13545 thread=70260187418160 request=16273] -- Handled
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Including data and exceptions
|
|
92
|
+
|
|
93
|
+
A Hash of data can be included along with the log message:
|
|
94
|
+
|
|
95
|
+
```ruby
|
|
96
|
+
logger = GreenLog::Logger.build
|
|
97
|
+
logger.info("New widget", id: widget.id)
|
|
98
|
+
# outputs: I -- New widget [id=12345]
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
And/or, you can attach an exception:
|
|
102
|
+
|
|
103
|
+
```ruby
|
|
104
|
+
begin
|
|
105
|
+
Integer("abc")
|
|
106
|
+
rescue => e
|
|
107
|
+
logger.error("parse error", e)
|
|
108
|
+
end
|
|
109
|
+
# outputs: E -- parse error
|
|
110
|
+
# ! ArgumentError: invalid value for Integer(): "abc"
|
|
111
|
+
# (irb):50:in `Integer'
|
|
112
|
+
# (irb):50:in `irb_binding'
|
|
113
|
+
# ...
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### Alternate output format
|
|
117
|
+
|
|
118
|
+
By default GreenLog logs with a human-readable format; specify an alternate `format`
|
|
119
|
+
class if you want a different serialisation format. It comes bundled with a JSON writer, e.g.
|
|
120
|
+
|
|
121
|
+
```ruby
|
|
122
|
+
logger = GreenLog::Logger.build(format: GreenLog::JsonWriter)
|
|
123
|
+
# OR
|
|
124
|
+
logger = GreenLog::Logger.build(format: "json")
|
|
125
|
+
|
|
126
|
+
logger.info("Structured!", foo: "bar")
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
outputs
|
|
130
|
+
|
|
131
|
+
```json
|
|
132
|
+
{"severity":"INFO","message":"Structured!","data":{"foo":"bar"},"context":{}}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### Alternate output destination
|
|
136
|
+
|
|
137
|
+
Logs go to STDOUT by default; specify `dest` to override, e.g.
|
|
138
|
+
|
|
139
|
+
```ruby
|
|
140
|
+
logger = GreenLog::Logger.build(dest: STDERR)
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Filtering by log severity
|
|
144
|
+
|
|
145
|
+
By default all log entries will result in output. You can add a severity-threshold to avoid emitting debug-level log messages, e.g.
|
|
146
|
+
|
|
147
|
+
```ruby
|
|
148
|
+
logger = GreenLog::Logger.build(severity_threshold: :INFO)
|
|
149
|
+
# OR
|
|
150
|
+
logger = GreenLog::Logger.build.with_severity_threshold(:INFO)
|
|
151
|
+
|
|
152
|
+
log.debug("Whatever") # ignored
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### Block form
|
|
156
|
+
|
|
157
|
+
Rather than passing arguments, you can provide a block to generate log messages:
|
|
158
|
+
|
|
159
|
+
```ruby
|
|
160
|
+
logger.info do
|
|
161
|
+
"generated message"
|
|
162
|
+
end
|
|
163
|
+
# outputs: I -- generated message
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
The block may be ignored, if a severity-threshold is in effect:
|
|
167
|
+
|
|
168
|
+
```ruby
|
|
169
|
+
logger = GreenLog::Logger.build(severity_threshold: :INFO)
|
|
170
|
+
|
|
171
|
+
log.debug do
|
|
172
|
+
# not evaluated
|
|
173
|
+
end
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### Compatibility with stdlib Logger
|
|
177
|
+
|
|
178
|
+
GreenLog includes a backward-compatibile adapter for code written to use Ruby's built-in [`Logger`](https://ruby-doc.org/stdlib-2.4.0/libdoc/logger/rdoc/Logger.html):
|
|
179
|
+
|
|
180
|
+
```ruby
|
|
181
|
+
require 'green_log/classic_logger'
|
|
182
|
+
|
|
183
|
+
legacy_logger = logger.to_classic_logger
|
|
184
|
+
legacy_logger.warn("Old skool")
|
|
185
|
+
# outputs: W -- Old skool
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
## Contributing
|
|
189
|
+
|
|
190
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/greensync/green_log.
|
|
191
|
+
|
|
192
|
+
## License
|
|
193
|
+
|
|
194
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/bin/console
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "bundler/setup"
|
|
5
|
+
require "green_log"
|
|
6
|
+
|
|
7
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
|
8
|
+
# with your gem easier. You can also use a different console, if you like.
|
|
9
|
+
|
|
10
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
|
11
|
+
# require "pry"
|
|
12
|
+
# Pry.start
|
|
13
|
+
|
|
14
|
+
require "irb"
|
|
15
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# 1. Record architecture decisions
|
|
2
|
+
|
|
3
|
+
Date: 2019-12-18
|
|
4
|
+
|
|
5
|
+
## Status
|
|
6
|
+
|
|
7
|
+
Accepted
|
|
8
|
+
|
|
9
|
+
## Context
|
|
10
|
+
|
|
11
|
+
We need to record the architectural decisions made on this project.
|
|
12
|
+
|
|
13
|
+
## Decision
|
|
14
|
+
|
|
15
|
+
We will use Architecture Decision Records, as [described by Michael Nygard](http://thinkrelevance.com/blog/2011/11/15/documenting-architecture-decisions).
|
|
16
|
+
|
|
17
|
+
## Consequences
|
|
18
|
+
|
|
19
|
+
See Michael Nygard's article, linked above. For a lightweight ADR toolset, see Nat Pryce's [adr-tools](https://github.com/npryce/adr-tools).
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# 2. Avoid global configuration
|
|
2
|
+
|
|
3
|
+
Date: 2019-12-18
|
|
4
|
+
|
|
5
|
+
## Status
|
|
6
|
+
|
|
7
|
+
Accepted
|
|
8
|
+
|
|
9
|
+
## Context
|
|
10
|
+
|
|
11
|
+
Applications require a way to configure their logging, e.g. what should be logged, where should logs be sent, and how should they be formatted.
|
|
12
|
+
|
|
13
|
+
Some Ruby logging libraries involve global configuration, and other state, e.g.
|
|
14
|
+
|
|
15
|
+
```ruby
|
|
16
|
+
require 'some_logger'
|
|
17
|
+
SomeLogger.default_level = :trace
|
|
18
|
+
SomeLogger.add_appender(file_name: 'development.log', formatter: :color)
|
|
19
|
+
SomeLogger["MyClass"].info("Stuff happened")
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
but global state can have unintended consequences, and make testing difficult.
|
|
23
|
+
|
|
24
|
+
## Decision
|
|
25
|
+
|
|
26
|
+
GreenLog will avoid global configuration. `GreenLog::Logger` instances should be created and configured explicitly, and injected as dependencies where needed.
|
|
27
|
+
|
|
28
|
+
## Consequences
|
|
29
|
+
|
|
30
|
+
- Testing of logging logic should be straightforward.
|
|
31
|
+
- Libraries and applications can make their own logging arrangements.
|
|
32
|
+
|
|
33
|
+
BUT
|
|
34
|
+
|
|
35
|
+
- Applications will need to determine their own conventions for configuration of logging.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# 3. Decouple log generation from handling
|
|
2
|
+
|
|
3
|
+
Date: 2019-12-19
|
|
4
|
+
|
|
5
|
+
## Status
|
|
6
|
+
|
|
7
|
+
Accepted
|
|
8
|
+
|
|
9
|
+
## Context
|
|
10
|
+
|
|
11
|
+
We want the logging API used by applications to be consistent, while allowing for logs to be filed, forwarded, filtered and formatted in a variety of ways.
|
|
12
|
+
|
|
13
|
+
## Decision
|
|
14
|
+
|
|
15
|
+
De-couple generation of log message/entries from how they are handled.
|
|
16
|
+
|
|
17
|
+
* A `logger` object provides an API that can be used to generate log entries.
|
|
18
|
+
* Log "entries" are strongly typed structures.
|
|
19
|
+
* Log entry "handlers" provide a simple, consistent interface.
|
|
20
|
+
|
|
21
|
+
```mermaid
|
|
22
|
+
sequenceDiagram
|
|
23
|
+
|
|
24
|
+
App ->> Logger: info("Message")
|
|
25
|
+
Logger ->> Handler: <<(entry)
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Consequences
|
|
29
|
+
|
|
30
|
+
* We can plug in different "handlers" relatively easily.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# 4. Use stacked handlers to solve many problems
|
|
2
|
+
|
|
3
|
+
Date: 2019-12-19
|
|
4
|
+
|
|
5
|
+
## Status
|
|
6
|
+
|
|
7
|
+
Accepted
|
|
8
|
+
|
|
9
|
+
## Context
|
|
10
|
+
|
|
11
|
+
GreenLog "loggers" push generated log "entries" to log "handlers".
|
|
12
|
+
|
|
13
|
+
Formatting/filing/forwarding logs in different ways is achieved by using a different handler.
|
|
14
|
+
|
|
15
|
+
However, there are some cross-cutting concerns that might apply regardless of the way log entries are formatted, stored or forwarded. For example:
|
|
16
|
+
|
|
17
|
+
- we may want to inject context into log entries (e.g. process id, thread name, trace id)
|
|
18
|
+
- we may want to "tee" log entries to multiple destinations
|
|
19
|
+
- we may want to buffer log entries in-memory and output/forward them asyncronously
|
|
20
|
+
|
|
21
|
+
## Decision
|
|
22
|
+
|
|
23
|
+
We will favour "stacking" handlers to solve such problems. That is:
|
|
24
|
+
|
|
25
|
+
* the source 'logger' will push entries to an intermediate handler
|
|
26
|
+
* the intermediate handler can
|
|
27
|
+
- transform the input entries as required
|
|
28
|
+
- forward them to one or more "downstream" handlers, as required
|
|
29
|
+
* handler "stacks" of arbitrary depth can be created
|
|
30
|
+
|
|
31
|
+
```mermaid
|
|
32
|
+
sequenceDiagram
|
|
33
|
+
|
|
34
|
+
App ->> Logger: info("Message")
|
|
35
|
+
Logger ->> Middleware: <<(entry)
|
|
36
|
+
Middleware ->> Writer: <<(entry)
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
This approach is based on [Rack](https://github.com/rack/rack)'s "middleware" concept.
|
|
40
|
+
|
|
41
|
+
### Alternatives considered
|
|
42
|
+
|
|
43
|
+
I considered supporting "filters", which could take an `Entry` and return a transformed `Entry`, but that would not allow for:
|
|
44
|
+
|
|
45
|
+
* "tee-ing" the log to multiple destinations
|
|
46
|
+
* buffering or filtering of logs
|
|
47
|
+
|
|
48
|
+
## Consequences
|
|
49
|
+
|
|
50
|
+
* There will be many and various "middleware" handlers.
|
|
51
|
+
* Handler stacks could get deep.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# 5. Restrict data-types allowable in log entries
|
|
2
|
+
|
|
3
|
+
Date: 2019-12-21
|
|
4
|
+
|
|
5
|
+
## Status
|
|
6
|
+
|
|
7
|
+
Accepted
|
|
8
|
+
|
|
9
|
+
## Context
|
|
10
|
+
|
|
11
|
+
GreenLog allows data to be attached to log entries, either as "context", or "data" associated with the logged event.
|
|
12
|
+
|
|
13
|
+
Log entries may be consumed by a variety of "handlers", which
|
|
14
|
+
|
|
15
|
+
## Decision
|
|
16
|
+
|
|
17
|
+
To make the job of "handlers" easier, GreenLog will restrict the type of value usable as log "context" or "data" to:
|
|
18
|
+
|
|
19
|
+
- `true`/`false`
|
|
20
|
+
- `Numeric`
|
|
21
|
+
- `String`
|
|
22
|
+
- `Time`
|
|
23
|
+
- `Hash` (with `String` or `Symbol` keys)
|
|
24
|
+
|
|
25
|
+
## Consequences
|
|
26
|
+
|
|
27
|
+
- Other data must be converted to one of these types before inclusion in log entries.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# 6. Use lock-free IO
|
|
2
|
+
|
|
3
|
+
Date: 2019-12-24
|
|
4
|
+
|
|
5
|
+
## Status
|
|
6
|
+
|
|
7
|
+
Accepted
|
|
8
|
+
|
|
9
|
+
## Context
|
|
10
|
+
|
|
11
|
+
We want to be able to write log entries (to file, or STDOUT), without them being interleaved.
|
|
12
|
+
|
|
13
|
+
But also, we want logging to perform well.
|
|
14
|
+
|
|
15
|
+
## Decision
|
|
16
|
+
|
|
17
|
+
_Unlike_ the Ruby standard `Logger`, GreenLog will use a [lock-free logging](https://www.jstorimer.com/blogs/workingwithcode/7982047-is-lock-free-logging-safe) approach. That is, we will:
|
|
18
|
+
|
|
19
|
+
- avoid using of mutexes to serialise output
|
|
20
|
+
- perform atomic writes to `IO` streams (using `<<`)
|
|
21
|
+
|
|
22
|
+
## Consequences
|
|
23
|
+
|
|
24
|
+
- IO code is simple.
|
|
25
|
+
- Performance should be good.
|
|
26
|
+
|
|
27
|
+
But:
|
|
28
|
+
|
|
29
|
+
- We risk interleaving of log entries if the size of the (serialized) entries gets over `PIPE_BUF_MAX` (1Mb on Linux).
|
|
@@ -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/simple_writer"
|
|
8
|
+
|
|
9
|
+
logger = GreenLog::Logger.new(GreenLog::SimpleWriter.new(STDOUT))
|
|
10
|
+
|
|
11
|
+
threads = ("A".."Z").map do |label|
|
|
12
|
+
Thread.new do
|
|
13
|
+
1.upto(100) do
|
|
14
|
+
delay = rand / 10
|
|
15
|
+
logger.info("Hi", thread: label, delay: delay)
|
|
16
|
+
sleep(delay)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
threads.each(&:join)
|