green_log 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
![](https://github.com/greensync/green_log/workflows/CI/badge.svg)
|
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)
|