logcraft 1.0.0.rc
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/CHANGELOG.md +9 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/Gemfile +13 -0
- data/LICENSE.txt +21 -0
- data/README.md +331 -0
- data/Rakefile +50 -0
- data/gemfiles/rails_5.2.gemfile +12 -0
- data/gemfiles/rails_6.0.gemfile +12 -0
- data/gemfiles/rails_6.1.gemfile +13 -0
- data/lib/logcraft/log_context_helper.rb +16 -0
- data/lib/logcraft/log_layout.rb +67 -0
- data/lib/logcraft/rails/active_record/log_subscriber.rb +54 -0
- data/lib/logcraft/rails/active_record.rb +9 -0
- data/lib/logcraft/rails/extensions.rb +12 -0
- data/lib/logcraft/rails/log_subscription_handler.rb +32 -0
- data/lib/logcraft/rails/request_id_logger.rb +19 -0
- data/lib/logcraft/rails/request_logger.rb +90 -0
- data/lib/logcraft/rails.rb +13 -0
- data/lib/logcraft/railtie.rb +48 -0
- data/lib/logcraft/rspec/helpers.rb +15 -0
- data/lib/logcraft/rspec/matchers.rb +61 -0
- data/lib/logcraft/rspec.rb +15 -0
- data/lib/logcraft/version.rb +5 -0
- data/lib/logcraft.rb +23 -0
- data/logcraft.gemspec +37 -0
- metadata +142 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: cebda0be923bafa1e3409178e92baa0c0161540b94655c1ccf0cc756ff7dbc2f
|
4
|
+
data.tar.gz: 35bcecda73c4bca768c4178fa5a0f1fe478d142b93f91f63ef8c8835dbc7d3f0
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 4ee4ea6eafd4e3b4ca5ff71b542baf38a58685ae639e083ece215a7e3f27d8c4b4fd08569f07e62d96602a5f37d7495478be90b7844adfa28b8caf3c6df7c829
|
7
|
+
data.tar.gz: 16f1d50ab12098b2e2ab341b4ff659f0504ff8282e8b78fe8cfe8a1ab8d8413e6d413b1ac4912e9e3af76080d7cc7407174a1b9a713b1922785180b562911f6a
|
data/.rspec
ADDED
data/CHANGELOG.md
ADDED
@@ -0,0 +1,9 @@
|
|
1
|
+
# Changelog
|
2
|
+
All notable changes to this project will be documented in this file.
|
3
|
+
|
4
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
5
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
6
|
+
|
7
|
+
## [1.0.0.rc] - 2022-06-26
|
8
|
+
### Added
|
9
|
+
- Logcraft was rewritten from the ground up, based on its predecessor: [Ezlog](https://github.com/emartech/ezlog)
|
data/CODE_OF_CONDUCT.md
ADDED
@@ -0,0 +1,84 @@
|
|
1
|
+
# Contributor Covenant Code of Conduct
|
2
|
+
|
3
|
+
## Our Pledge
|
4
|
+
|
5
|
+
We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation.
|
6
|
+
|
7
|
+
We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community.
|
8
|
+
|
9
|
+
## Our Standards
|
10
|
+
|
11
|
+
Examples of behavior that contributes to a positive environment for our community include:
|
12
|
+
|
13
|
+
* Demonstrating empathy and kindness toward other people
|
14
|
+
* Being respectful of differing opinions, viewpoints, and experiences
|
15
|
+
* Giving and gracefully accepting constructive feedback
|
16
|
+
* Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience
|
17
|
+
* Focusing on what is best not just for us as individuals, but for the overall community
|
18
|
+
|
19
|
+
Examples of unacceptable behavior include:
|
20
|
+
|
21
|
+
* The use of sexualized language or imagery, and sexual attention or
|
22
|
+
advances of any kind
|
23
|
+
* Trolling, insulting or derogatory comments, and personal or political attacks
|
24
|
+
* Public or private harassment
|
25
|
+
* Publishing others' private information, such as a physical or email
|
26
|
+
address, without their explicit permission
|
27
|
+
* Other conduct which could reasonably be considered inappropriate in a
|
28
|
+
professional setting
|
29
|
+
|
30
|
+
## Enforcement Responsibilities
|
31
|
+
|
32
|
+
Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful.
|
33
|
+
|
34
|
+
Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate.
|
35
|
+
|
36
|
+
## Scope
|
37
|
+
|
38
|
+
This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event.
|
39
|
+
|
40
|
+
## Enforcement
|
41
|
+
|
42
|
+
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement via https://github.com/zormandi/logcraft. All complaints will be reviewed and investigated promptly and fairly.
|
43
|
+
|
44
|
+
All community leaders are obligated to respect the privacy and security of the reporter of any incident.
|
45
|
+
|
46
|
+
## Enforcement Guidelines
|
47
|
+
|
48
|
+
Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct:
|
49
|
+
|
50
|
+
### 1. Correction
|
51
|
+
|
52
|
+
**Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community.
|
53
|
+
|
54
|
+
**Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested.
|
55
|
+
|
56
|
+
### 2. Warning
|
57
|
+
|
58
|
+
**Community Impact**: A violation through a single incident or series of actions.
|
59
|
+
|
60
|
+
**Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban.
|
61
|
+
|
62
|
+
### 3. Temporary Ban
|
63
|
+
|
64
|
+
**Community Impact**: A serious violation of community standards, including sustained inappropriate behavior.
|
65
|
+
|
66
|
+
**Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban.
|
67
|
+
|
68
|
+
### 4. Permanent Ban
|
69
|
+
|
70
|
+
**Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals.
|
71
|
+
|
72
|
+
**Consequence**: A permanent ban from any sort of public interaction within the community.
|
73
|
+
|
74
|
+
## Attribution
|
75
|
+
|
76
|
+
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0,
|
77
|
+
available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
78
|
+
|
79
|
+
Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity).
|
80
|
+
|
81
|
+
[homepage]: https://www.contributor-covenant.org
|
82
|
+
|
83
|
+
For answers to common questions about this code of conduct, see the FAQ at
|
84
|
+
https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations.
|
data/Gemfile
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
source "https://rubygems.org"
|
4
|
+
|
5
|
+
# Specify your gem's dependencies in logcraft.gemspec
|
6
|
+
gemspec
|
7
|
+
|
8
|
+
group :test do
|
9
|
+
gem 'rails', '~> 6.1'
|
10
|
+
gem 'rspec-rails', '~> 4.0'
|
11
|
+
gem 'sqlite3', '~> 1.4'
|
12
|
+
gem 'net-smtp', require: false
|
13
|
+
end
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2022 Zoltan Ormandi
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,331 @@
|
|
1
|
+
# Logcraft
|
2
|
+
|
3
|
+
[![Build Status](https://github.com/zormandi/logcraft/actions/workflows/main.yml/badge.svg)](https://github.com/zormandi/logcraft/actions/workflows/main.yml)
|
4
|
+
|
5
|
+
Logcraft is a zero-configuration structured logging library for pure Ruby or [Ruby on Rails](https://rubyonrails.org/) applications.
|
6
|
+
It is the successor to [Ezlog](https://github.com/emartech/ezlog) with which it shares its ideals but is reimagined and
|
7
|
+
reimplemented to be more versatile and much more thoroughly tested.
|
8
|
+
|
9
|
+
Logcraft's purpose is threefold:
|
10
|
+
1. Make sure that our applications are logging in a concise and sensible manner; emitting no unnecessary "noise" but
|
11
|
+
containing all relevant and necessary information (like timing or a request ID).
|
12
|
+
2. Make sure that all log messages are written to STDOUT in a machine-processable format (JSON).
|
13
|
+
3. Achieving the above goals should require no configuration in the projects where the library is used.
|
14
|
+
|
15
|
+
Logcraft supports:
|
16
|
+
* [Ruby](https://www.ruby-lang.org) 2.6 and up (tested with 2.6, 2.7, 3.0 and 3.1)
|
17
|
+
* [Rails](https://rubyonrails.org/) 5 and up (tested with 5.2, 6.0 and 6.1)
|
18
|
+
* [Sidekiq](https://github.com/mperham/sidekiq) support is coming soon via a separate gem (_logcraft-sidekiq_)
|
19
|
+
|
20
|
+
Logcraft uses Tim Pease's wonderful [Logging](https://github.com/TwP/logging) gem under the hood for an all-purpose structured logging solution.
|
21
|
+
|
22
|
+
## Installation
|
23
|
+
|
24
|
+
### Rails
|
25
|
+
|
26
|
+
Add this line to your application's Gemfile:
|
27
|
+
```ruby
|
28
|
+
gem 'logcraft'
|
29
|
+
```
|
30
|
+
|
31
|
+
Although Logcraft sets up sensible defaults for all logging configuration settings, it leaves you the option to override these
|
32
|
+
settings manually in the way you're used to; via Rails's configuration mechanism. Unfortunately the Rails new project generator
|
33
|
+
automatically generates code for the production environment configuration that overrides some of these default settings.
|
34
|
+
|
35
|
+
For Logcraft to work properly, you need to delete or comment out the logging configuration options in the generated
|
36
|
+
`config/environments/production.rb` file.
|
37
|
+
|
38
|
+
### Non-Rails applications
|
39
|
+
|
40
|
+
Add this line to your application's Gemfile:
|
41
|
+
```ruby
|
42
|
+
gem 'logcraft'
|
43
|
+
```
|
44
|
+
and call
|
45
|
+
```ruby
|
46
|
+
Logcraft.initialize
|
47
|
+
```
|
48
|
+
any time during your application's startup.
|
49
|
+
|
50
|
+
## Usage
|
51
|
+
|
52
|
+
### Structured logging
|
53
|
+
|
54
|
+
Any loggers created by your application (including the `Rails.logger`) will automatically be configured to write
|
55
|
+
messages in JSON format to the standard output. These loggers can handle a variety of message types:
|
56
|
+
|
57
|
+
* String
|
58
|
+
* Hash
|
59
|
+
* Exception
|
60
|
+
* any other object that can be coerced into a String
|
61
|
+
|
62
|
+
The logger also automatically adds some basic information to all messages, such as:
|
63
|
+
* name of the logger
|
64
|
+
* timestamp
|
65
|
+
* log level (as string)
|
66
|
+
* hostname
|
67
|
+
* PID
|
68
|
+
|
69
|
+
Examples:
|
70
|
+
```ruby
|
71
|
+
logger = Logcraft.logger 'Application'
|
72
|
+
|
73
|
+
logger.info 'Log message'
|
74
|
+
# => {"timestamp":"2022-06-26T17:52:57.845+02:00","level":"INFO","logger":"Application","hostname":"Zoltans-iPro","pid":80422,"message":"Log message"}
|
75
|
+
|
76
|
+
logger.info message: 'User logged in', user_id: 42
|
77
|
+
# => {"timestamp":"2022-06-26T17:44:01.926+02:00","level":"INFO","logger":"Application","hostname":"MacbookPro.local","pid":80422,"message":"User logged in","user_id":42}
|
78
|
+
|
79
|
+
logger.warn error
|
80
|
+
# Formatted for better readability (the original is a single line string):
|
81
|
+
# => {
|
82
|
+
# "timestamp": "2022-06-26T17:46:42.418+02:00",
|
83
|
+
# "level": "WARN",
|
84
|
+
# "logger": "Application",
|
85
|
+
# "hostname": "MacbookPro.local",
|
86
|
+
# "pid": 80422,
|
87
|
+
# "message": "wrapping error",
|
88
|
+
# "error": {
|
89
|
+
# "class": "StandardError",
|
90
|
+
# "message": "wrapping error",
|
91
|
+
# "backtrace": [...],
|
92
|
+
# "cause": {
|
93
|
+
# "class": "RuntimeError",
|
94
|
+
# "message": "original error",
|
95
|
+
# "backtrace": [...]
|
96
|
+
# }
|
97
|
+
# }
|
98
|
+
# }
|
99
|
+
```
|
100
|
+
|
101
|
+
#### Adding context information to log messages
|
102
|
+
|
103
|
+
Logcraft provides two helper methods which can be used to add context information to log messages:
|
104
|
+
|
105
|
+
* `within_log_context(context)`: Starts a new log context initialized with `context` and executes the provided block
|
106
|
+
within that context. Once execution is finished, the log context is cleaned up and the previous context (if any) is
|
107
|
+
reinstated. In practice, this means that every time we log something (within the block), the log message will include
|
108
|
+
the information that's in the current context. This can be useful for storing request-specific information
|
109
|
+
(request ID, user ID, ...) in the log context early on (for example in a middleware) and not have to worry about
|
110
|
+
including it every time we want to log a message.
|
111
|
+
|
112
|
+
Example:
|
113
|
+
|
114
|
+
```ruby
|
115
|
+
within_log_context customer_id: 1234 do
|
116
|
+
logger.info 'test 1'
|
117
|
+
end
|
118
|
+
logger.info 'test 2'
|
119
|
+
|
120
|
+
#=> {...,"level":"INFO","customer_id":1234,"message":"test 1"}
|
121
|
+
#=> {...,"level":"INFO","message":"test 2"}
|
122
|
+
```
|
123
|
+
|
124
|
+
* `add_to_log_context(context)`: Adds the provided `context` to the current log context but provides no mechanism for
|
125
|
+
removing it later. Only use this method if you are sure that you're working within a specific log context and that it
|
126
|
+
will be cleaned up later (e.g. by only using this method in a block passed to the previously explained
|
127
|
+
`within_log_context` method).
|
128
|
+
|
129
|
+
You can access these methods either in the global scope by calling them via `Logcraft.within_log_context` and
|
130
|
+
`Logcraft.add_to_log_context` or locally by including the `Logcraft::LogContextHelper` module into your class/module.
|
131
|
+
|
132
|
+
### Rails logging
|
133
|
+
|
134
|
+
Logcraft automatically configures Rails to provide you with structured logging capability via the `Rails.logger`.
|
135
|
+
It also changes Rails's default logging configuration to be more concise and emit less "noise".
|
136
|
+
|
137
|
+
In more detail:
|
138
|
+
* The `Rails.logger` is set up to be a Logcraft logger with the name `Application`.
|
139
|
+
* Rails's default logging of uncaught errors is modified and instead of spreading the error message across several lines,
|
140
|
+
Logcraft log every uncaught error in 1 line (per error), including the error's name and context (stack trace, etc.).
|
141
|
+
* Most importantly, Rails's default request logging - which logs several lines per event during the processing of an action -
|
142
|
+
is replaced by Logcraft's own access log middleware. The end result is an access log that
|
143
|
+
* contains all relevant information (request ID, method, path, params, client IP, duration and response status code), and
|
144
|
+
* has 1 log line per request, logged at the end of the request.
|
145
|
+
|
146
|
+
Thanks to Mathias Meyer for writing [Lograge](https://github.com/roidrage/lograge), which inspired the solution.
|
147
|
+
If Logcraft is not your cup of tea but you're looking for a way to tame Rails's logging then be sure to check out
|
148
|
+
[Lograge](https://github.com/roidrage/lograge).
|
149
|
+
|
150
|
+
```
|
151
|
+
GET /welcome?subsession_id=34ea8596f9764f475f81158667bc2654
|
152
|
+
|
153
|
+
With default Rails logging:
|
154
|
+
|
155
|
+
Started GET "/welcome?subsession_id=34ea8596f9764f475f81158667bc2654" for 127.0.0.1 at 2022-06-26 18:07:08 +0200
|
156
|
+
Processing by PagesController#welcome as HTML
|
157
|
+
Parameters: {"subsession_id"=>"34ea8596f9764f475f81158667bc2654"}
|
158
|
+
Rendering pages/welcome.html.haml within layouts/application
|
159
|
+
Rendered pages/welcome.html.haml within layouts/application (5.5ms)
|
160
|
+
Completed 200 OK in 31ms (Views: 27.3ms | ActiveRecord: 0.0ms)
|
161
|
+
|
162
|
+
With Logcraft:
|
163
|
+
{"timestamp":"2022-06-26T18:07:08.103+02:00","level":"INFO","logger":"AccessLog","hostname":"MacbookPro.local","pid":80908,"request_id":"9a43631b-284c-4677-9d08-9c1cc5c7d3a7","message":"GET /welcome?subsession_id=34ea8596f9764f475f81158667bc2654 - 200 (OK)","remote_ip":"127.0.0.1","method":"GET","path":"/welcome?subsession_id=34ea8596f9764f475f81158667bc2654","params":{"subsession_id":"34ea8596f9764f475f81158667bc2654","controller":"pages","action":"welcome"},"response_status_code":200,"duration":13,"duration_sec":0.013}
|
164
|
+
|
165
|
+
Formatted for readability:
|
166
|
+
{
|
167
|
+
"timestamp": "2022-06-26T18:07:08.103+02:00",
|
168
|
+
"level": "INFO",
|
169
|
+
"logger": "AccessLog",
|
170
|
+
"hostname": "MacbookPro.local",
|
171
|
+
"pid": 80908, "request_id": "9a43631b-284c-4677-9d08-9c1cc5c7d3a7",
|
172
|
+
"message": "GET /welcome?subsession_id=34ea8596f9764f475f81158667bc2654 - 200 (OK)",
|
173
|
+
"remote_ip": "127.0.0.1",
|
174
|
+
"method": "GET",
|
175
|
+
"path": "/welcome?subsession_id=34ea8596f9764f475f81158667bc2654",
|
176
|
+
"params": {
|
177
|
+
"subsession_id": "34ea8596f9764f475f81158667bc2654",
|
178
|
+
"controller": "pages",
|
179
|
+
"action": "welcome"
|
180
|
+
},
|
181
|
+
"response_status_code": 200,
|
182
|
+
"duration": 13,
|
183
|
+
"duration_sec": 0.013
|
184
|
+
}
|
185
|
+
```
|
186
|
+
|
187
|
+
By default, Logcraft logs all request parameters as a hash (JSON object) under the `params` key. This is very convenient
|
188
|
+
in a structured logging system and makes it easy to search for specific request parameter values e.g. in ElasticSearch
|
189
|
+
(should you happen to store your logs there). Unfortunately, in some cases - such as when handling large forms - this
|
190
|
+
can create quite a bit of noise and impact the searchability of your logs negatively or pose a security risk or data policy
|
191
|
+
violation. You have the option to restrict the logging of certain parameters via configuration options (see the
|
192
|
+
Configuration section).
|
193
|
+
|
194
|
+
#### The log level
|
195
|
+
|
196
|
+
The logger's log level is determined as follows (in order of precedence):
|
197
|
+
* the log level set in the application's configuration (for Rails applications),
|
198
|
+
* the LOG_LEVEL environment variable, or
|
199
|
+
* `INFO` as the default log level if none of the above are set.
|
200
|
+
|
201
|
+
The following log levels are available: `DEBUG`, `INFO`, `WARN`, `ERROR`, `FATAL`.
|
202
|
+
|
203
|
+
## Configuration options
|
204
|
+
|
205
|
+
### Rails
|
206
|
+
|
207
|
+
Logcraft provides the following configuration options for Rails:
|
208
|
+
|
209
|
+
| Option | Default value | Description |
|
210
|
+
|-------------------------------------------------|--------------------------|-------------------------------------------------------------------------------------------------------------|
|
211
|
+
| logcraft.initial_context | `{}` | A global log context that will be included in every log message. May include lambdas (see examples). |
|
212
|
+
| logcraft.layout_options | `{}` | Custom options for the log layout. Currently only the `level_formatter` option is supported (see examples). |
|
213
|
+
| logcraft.access_log.logger_name | `'AccessLog'` | The name of the logger emitting access log messages. |
|
214
|
+
| logcraft.access_log.exclude_paths | `[]` | A list of paths (array of strings or RegExps) not to include in the access log. |
|
215
|
+
| logcraft.access_log.log_only_whitelisted_params | `false` | If `true`, the access log will only contain whitelisted parameters. |
|
216
|
+
| logcraft.access_log.whitelisted_params | `[:controller, :action]` | The only parameters to be logged in the access log if whitelisting is enabled. |
|
217
|
+
|
218
|
+
Examples:
|
219
|
+
```ruby
|
220
|
+
# Use these options in your Rails configuration files (e.g. application.rb)
|
221
|
+
|
222
|
+
# Set up a global context you want to see in every log message
|
223
|
+
config.logcraft.initial_context = {
|
224
|
+
environment: ENV['RAILS_ENV'],
|
225
|
+
timestamp_linux: -> { Time.current.to_i } # evaluated when emitting a log message
|
226
|
+
}
|
227
|
+
|
228
|
+
# Set up a custom log level formatter (e.g. Ougai-like numbers)
|
229
|
+
config.logcraft.layout_options = {
|
230
|
+
level_formatter: ->(level_number) { (level_number + 2) * 10 }
|
231
|
+
}
|
232
|
+
Rails.logger.error('Boom!')
|
233
|
+
# => {...,"level":50,"message":"Boom!"}
|
234
|
+
|
235
|
+
# Exclude healthcheck and monitoring URLs from your access log:
|
236
|
+
config.logcraft.exclude_paths = ['/healthcheck', %r(/monitoring/.*)]
|
237
|
+
|
238
|
+
# Make sure no sensitive data is logged by accident in the access log, so only log controller and action:
|
239
|
+
config.logcraft.log_only_whitelisted_params = true
|
240
|
+
```
|
241
|
+
|
242
|
+
### Non-Rails
|
243
|
+
|
244
|
+
The `initial_context` and `layout_options` configuration options (see above) are available to non-Rails projects
|
245
|
+
via Logcraft's initialization mechanism. You can also set the default log level this way.
|
246
|
+
|
247
|
+
```ruby
|
248
|
+
Logcraft.initialize log_level: :info, initial_context: {}, layout_options: {}
|
249
|
+
```
|
250
|
+
|
251
|
+
## Integration with DataDog
|
252
|
+
|
253
|
+
You can set up tracing with [DataDog](https://www.datadoghq.com/) by providing an initial context to be included in every log message:
|
254
|
+
|
255
|
+
```ruby
|
256
|
+
config.logcraft.initial_context = {
|
257
|
+
dd: -> do
|
258
|
+
return unless Datadog::Tracing.enabled?
|
259
|
+
|
260
|
+
correlation = Datadog::Tracing.correlation
|
261
|
+
{
|
262
|
+
trace_id: correlation.trace_id.to_s,
|
263
|
+
span_id: correlation.span_id.to_s,
|
264
|
+
env: correlation.env.to_s,
|
265
|
+
service: correlation.service.to_s,
|
266
|
+
version: correlation.version.to_s
|
267
|
+
}
|
268
|
+
end,
|
269
|
+
ddsource: ['ruby']
|
270
|
+
}
|
271
|
+
```
|
272
|
+
|
273
|
+
## RSpec support
|
274
|
+
|
275
|
+
Logcraft comes with built-in support for testing your logging activity using [RSpec](https://rspec.info/).
|
276
|
+
To enable spec support for Logcraft, put this line in your `spec_helper.rb` or `rails_helper.rb`:
|
277
|
+
|
278
|
+
```ruby
|
279
|
+
require 'logcraft/rspec'
|
280
|
+
```
|
281
|
+
|
282
|
+
What you get:
|
283
|
+
* Helpers
|
284
|
+
* `log_output` provides access to the complete log output (array of strings) in your specs
|
285
|
+
* `log_output_is_expected` shorthand for writing expectations for the log output
|
286
|
+
* Matchers
|
287
|
+
* `include_log_message` matcher for expecting a certain message in the log output
|
288
|
+
* `log` matcher for expecting an operation to log a certain message
|
289
|
+
|
290
|
+
```ruby
|
291
|
+
# Check that the log output contains a certain message
|
292
|
+
expect(log_output).to include_log_message message: 'Test message'
|
293
|
+
log_output_is_expected.to include_log_message message: 'Test message'
|
294
|
+
|
295
|
+
# Check that the message is not present in the logs before the operation but is present after it
|
296
|
+
expect { operation }.to log message: 'Test message',
|
297
|
+
user_id: 123456
|
298
|
+
|
299
|
+
# Expect a certain log level
|
300
|
+
log_output_is_expected.to include_log_message(message: 'Test message').at_level(:info)
|
301
|
+
expect { operation }.to log(message: 'Test message').at_level(:info)
|
302
|
+
```
|
303
|
+
|
304
|
+
## Development
|
305
|
+
|
306
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests.
|
307
|
+
You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
308
|
+
|
309
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version,
|
310
|
+
update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag
|
311
|
+
for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
312
|
+
|
313
|
+
## Contributing
|
314
|
+
|
315
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/zormandi/logcraft. This project is intended
|
316
|
+
to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the
|
317
|
+
[code of conduct](https://github.com/zormandi/logcraft/blob/master/CODE_OF_CONDUCT.md).
|
318
|
+
|
319
|
+
## Disclaimer
|
320
|
+
|
321
|
+
Logcraft is highly opinionated software and does in no way aim or claim to be useful for everyone.
|
322
|
+
Use at your own discretion.
|
323
|
+
|
324
|
+
## License
|
325
|
+
|
326
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
327
|
+
|
328
|
+
## Code of Conduct
|
329
|
+
|
330
|
+
Everyone interacting in the Logcraft project's codebases, issue trackers, chat rooms and mailing lists is expected
|
331
|
+
to follow the [code of conduct](https://github.com/zormandi/logcraft/blob/master/CODE_OF_CONDUCT.md).
|
data/Rakefile
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "bundler/gem_tasks"
|
4
|
+
require "rspec/core/rake_task"
|
5
|
+
|
6
|
+
def add_logcraft_options_to_application_configuration
|
7
|
+
project_root = File.dirname __FILE__
|
8
|
+
Dir.chdir(project_root + '/spec') do
|
9
|
+
app_config = File.readlines 'test-app/config/application.rb'
|
10
|
+
modified_config = app_config.each_with_object([]) do |line, config|
|
11
|
+
config << line
|
12
|
+
if line.include? 'config.load_defaults'
|
13
|
+
logcraft_config = File.readlines 'fixtures/test-app/config/logcraft_config.rb'
|
14
|
+
config.concat logcraft_config
|
15
|
+
end
|
16
|
+
end
|
17
|
+
File.write 'test-app/config/application.rb', modified_config.join
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
desc 'Generate sample Rails app for acceptance testing'
|
22
|
+
task :generate_rails_app do
|
23
|
+
project_root = File.dirname __FILE__
|
24
|
+
Dir.chdir(project_root + '/spec') do
|
25
|
+
FileUtils.rm_rf 'test-app'
|
26
|
+
system 'rails new test-app --database=sqlite3 --skip-gemfile --skip-git --skip-keeps --skip-action-mailer'\
|
27
|
+
'--skip-action-mailbox --skip-action-text --skip-active-job --skip-active-storage --skip-puma --skip-action-cable'\
|
28
|
+
'--skip-sprockets --skip-spring --skip-listen --skip-javascript --skip-turbolinks --skip-jbuilder --skip-test'\
|
29
|
+
'--skip-system-test --skip-bootsnap --skip-bundle --skip-webpack-install'
|
30
|
+
FileUtils.cp_r 'fixtures/test-app/.', 'test-app', remove_destination: true
|
31
|
+
add_logcraft_options_to_application_configuration
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
namespace :spec do
|
36
|
+
desc 'Run RSpec unit tests'
|
37
|
+
RSpec::Core::RakeTask.new(:unit) do |t|
|
38
|
+
t.exclude_pattern = 'spec/integration/*_spec.rb'
|
39
|
+
end
|
40
|
+
|
41
|
+
desc 'Run RSpec integration tests'
|
42
|
+
RSpec::Core::RakeTask.new(:integration) do |t|
|
43
|
+
t.pattern = 'spec/integration/*_spec.rb'
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
desc 'Run all RSpec examples'
|
48
|
+
RSpec::Core::RakeTask.new(:spec)
|
49
|
+
|
50
|
+
task default: :spec
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
source "https://rubygems.org"
|
4
|
+
|
5
|
+
# Specify your gem's dependencies in logcraft.gemspec
|
6
|
+
gemspec path: '..'
|
7
|
+
|
8
|
+
group :test do
|
9
|
+
gem 'rails', '~> 6.1.0'
|
10
|
+
gem 'rspec-rails', '~> 4.0'
|
11
|
+
gem 'sqlite3', '~> 1.4'
|
12
|
+
gem 'net-smtp', require: false
|
13
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Logcraft
|
4
|
+
module LogContextHelper
|
5
|
+
def within_log_context(context = {})
|
6
|
+
Logging.mdc.push context
|
7
|
+
yield
|
8
|
+
ensure
|
9
|
+
Logging.mdc.pop
|
10
|
+
end
|
11
|
+
|
12
|
+
def add_to_log_context(context)
|
13
|
+
Logging.mdc.update context
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'time'
|
4
|
+
|
5
|
+
module Logcraft
|
6
|
+
class LogLayout < Logging::Layout
|
7
|
+
def initialize(context = {}, options = {})
|
8
|
+
@general_context = context
|
9
|
+
@level_formatter = options.fetch :level_formatter, ->(level) { Logging::LNAMES[level] }
|
10
|
+
end
|
11
|
+
|
12
|
+
def format(event)
|
13
|
+
log_entry = background_of(event).merge evaluated_general_context,
|
14
|
+
dynamic_log_context,
|
15
|
+
message_from(event.data)
|
16
|
+
MultiJson.dump(log_entry) + "\n"
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def background_of(event)
|
22
|
+
{
|
23
|
+
'timestamp' => event.time.iso8601(3),
|
24
|
+
'level' => @level_formatter.call(event.level),
|
25
|
+
'logger' => event.logger,
|
26
|
+
'hostname' => Socket.gethostname,
|
27
|
+
'pid' => Process.pid
|
28
|
+
}
|
29
|
+
end
|
30
|
+
|
31
|
+
def evaluated_general_context
|
32
|
+
@general_context.transform_values { |v| v.is_a?(Proc) ? v.call : v }
|
33
|
+
end
|
34
|
+
|
35
|
+
def dynamic_log_context
|
36
|
+
Logging.mdc.context
|
37
|
+
end
|
38
|
+
|
39
|
+
def message_from(payload)
|
40
|
+
case payload
|
41
|
+
when Hash
|
42
|
+
format_hash payload
|
43
|
+
when Exception
|
44
|
+
{'message' => payload.message, 'error' => format_exception(payload)}
|
45
|
+
else
|
46
|
+
{'message' => payload}
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def format_hash(hash)
|
51
|
+
hash.transform_values { |v| v.is_a?(Exception) ? format_exception(v) : v }
|
52
|
+
end
|
53
|
+
|
54
|
+
def format_exception(exception)
|
55
|
+
error_hash = {'class' => exception.class.name,
|
56
|
+
'message' => exception.message}
|
57
|
+
error_hash['backtrace'] = exception.backtrace.first(20) if exception.backtrace
|
58
|
+
error_hash['cause'] = format_cause(exception.cause) if exception.cause
|
59
|
+
error_hash
|
60
|
+
end
|
61
|
+
|
62
|
+
def format_cause(cause)
|
63
|
+
cause = cause.cause while cause.cause
|
64
|
+
format_exception cause
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Logcraft
|
4
|
+
module Rails
|
5
|
+
module ActiveRecord
|
6
|
+
class LogSubscriber < ActiveSupport::LogSubscriber
|
7
|
+
def sql(event)
|
8
|
+
::ActiveRecord::Base.logger.debug { log_message_from(event) }
|
9
|
+
end
|
10
|
+
|
11
|
+
private
|
12
|
+
|
13
|
+
def log_message_from(event)
|
14
|
+
basic_message_from(event).tap do |message|
|
15
|
+
params = params_from event
|
16
|
+
message[:params] = params if params.any?
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def basic_message_from(event)
|
21
|
+
{
|
22
|
+
message: "SQL - #{event.payload[:name] || 'Query'} (#{event.duration.round(3)}ms)",
|
23
|
+
sql: event.payload[:sql],
|
24
|
+
duration: event.duration,
|
25
|
+
duration_sec: (event.duration / 1000.0).round(5)
|
26
|
+
}
|
27
|
+
end
|
28
|
+
|
29
|
+
def params_from(event)
|
30
|
+
return {} if event.payload.fetch(:binds, []).empty?
|
31
|
+
|
32
|
+
params = event.payload[:binds]
|
33
|
+
values = type_casted_values_from event
|
34
|
+
param_value_pairs = params.zip(values).map do |param, value|
|
35
|
+
[param.name, value_of(param, value)]
|
36
|
+
end
|
37
|
+
|
38
|
+
Hash[param_value_pairs]
|
39
|
+
rescue NoMethodError
|
40
|
+
params
|
41
|
+
end
|
42
|
+
|
43
|
+
def type_casted_values_from(event)
|
44
|
+
binds = event.payload[:type_casted_binds]
|
45
|
+
binds.respond_to?(:call) ? binds.call : binds
|
46
|
+
end
|
47
|
+
|
48
|
+
def value_of(param, value)
|
49
|
+
param.type.binary? ? '-binary data-' : value
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
module ActionDispatch
|
2
|
+
class DebugExceptions
|
3
|
+
def log_error_with_logcraft(request, wrapper)
|
4
|
+
logger = logger(request)
|
5
|
+
exception = wrapper.exception
|
6
|
+
logger.fatal exception
|
7
|
+
end
|
8
|
+
|
9
|
+
alias_method :original_log_error, :log_error
|
10
|
+
alias_method :log_error, :log_error_with_logcraft
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rails/version'
|
4
|
+
|
5
|
+
module Logcraft
|
6
|
+
module Rails
|
7
|
+
class LogSubscriptionHandler
|
8
|
+
class << self
|
9
|
+
def detach(subscriber_class, namespace)
|
10
|
+
case ::Rails::VERSION::MAJOR
|
11
|
+
when 5
|
12
|
+
subscriber = ::ActiveSupport::LogSubscriber.log_subscribers.find { |subscriber| subscriber.is_a? subscriber_class }
|
13
|
+
return unless subscriber
|
14
|
+
|
15
|
+
subscriber.patterns.each do |pattern|
|
16
|
+
::ActiveSupport::Notifications.notifier.listeners_for(pattern).each do |listener|
|
17
|
+
::ActiveSupport::Notifications.unsubscribe listener if listener.instance_variable_get('@delegate').is_a? subscriber_class
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
else
|
22
|
+
subscriber_class.detach_from namespace
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def attach(subscriber_class, namespace)
|
27
|
+
subscriber_class.attach_to namespace
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Logcraft
|
4
|
+
module Rails
|
5
|
+
class RequestIdLogger
|
6
|
+
include LogContextHelper
|
7
|
+
|
8
|
+
def initialize(app)
|
9
|
+
@app = app
|
10
|
+
end
|
11
|
+
|
12
|
+
def call(env)
|
13
|
+
within_log_context request_id: env['action_dispatch.request_id'] do
|
14
|
+
@app.call env
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Logcraft
|
4
|
+
module Rails
|
5
|
+
class RequestLogger
|
6
|
+
def initialize(app, logger, config)
|
7
|
+
@app = app
|
8
|
+
@logger = logger
|
9
|
+
@config = config
|
10
|
+
end
|
11
|
+
|
12
|
+
def call(env)
|
13
|
+
start_time = current_time_in_milliseconds
|
14
|
+
request = ActionDispatch::Request.new env
|
15
|
+
|
16
|
+
instrumentation_start request
|
17
|
+
|
18
|
+
status, headers, body = @app.call env
|
19
|
+
body = ::Rack::BodyProxy.new(body) do
|
20
|
+
instrumentation_finish request
|
21
|
+
log_request request, status, start_time
|
22
|
+
end
|
23
|
+
|
24
|
+
[status, headers, body]
|
25
|
+
rescue Exception => ex
|
26
|
+
instrumentation_finish request
|
27
|
+
log_request request, status_for_error(ex), start_time
|
28
|
+
raise
|
29
|
+
ensure
|
30
|
+
ActiveSupport::LogSubscriber.flush_all!
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def current_time_in_milliseconds
|
36
|
+
Process.clock_gettime Process::CLOCK_MONOTONIC, :millisecond
|
37
|
+
end
|
38
|
+
|
39
|
+
def instrumentation_start(request)
|
40
|
+
instrumenter = ActiveSupport::Notifications.instrumenter
|
41
|
+
instrumenter.start 'request.action_dispatch', request: request
|
42
|
+
end
|
43
|
+
|
44
|
+
def instrumentation_finish(request)
|
45
|
+
instrumenter = ActiveSupport::Notifications.instrumenter
|
46
|
+
instrumenter.finish 'request.action_dispatch', request: request
|
47
|
+
end
|
48
|
+
|
49
|
+
def log_request(request, status, start_time)
|
50
|
+
return if path_ignored? request
|
51
|
+
|
52
|
+
end_time = current_time_in_milliseconds
|
53
|
+
message = {
|
54
|
+
message: '%s %s - %i (%s)' % [request.method, request.filtered_path, status, Rack::Utils::HTTP_STATUS_CODES[status]],
|
55
|
+
remote_ip: request.remote_ip,
|
56
|
+
method: request.method,
|
57
|
+
path: request.filtered_path,
|
58
|
+
params: params_to_log(request),
|
59
|
+
response_status_code: status,
|
60
|
+
duration: end_time - start_time,
|
61
|
+
duration_sec: (end_time - start_time) / 1000.0
|
62
|
+
}
|
63
|
+
@logger.info message
|
64
|
+
end
|
65
|
+
|
66
|
+
def path_ignored?(request)
|
67
|
+
@config.exclude_paths.any? do |pattern|
|
68
|
+
case pattern
|
69
|
+
when Regexp
|
70
|
+
pattern.match? request.path
|
71
|
+
else
|
72
|
+
pattern == request.path
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def params_to_log(request)
|
78
|
+
if @config.log_only_whitelisted_params
|
79
|
+
request.filtered_parameters.slice *@config.whitelisted_params&.map(&:to_s)
|
80
|
+
else
|
81
|
+
request.filtered_parameters
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def status_for_error(error)
|
86
|
+
ActionDispatch::ExceptionWrapper.status_code_for_exception error.class.name
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'action_controller'
|
4
|
+
require 'action_controller/log_subscriber'
|
5
|
+
|
6
|
+
module Logcraft
|
7
|
+
module Rails
|
8
|
+
autoload :ActiveRecord, 'logcraft/rails/active_record'
|
9
|
+
autoload :RequestIdLogger, 'logcraft/rails/request_id_logger'
|
10
|
+
autoload :RequestLogger, 'logcraft/rails/request_logger'
|
11
|
+
autoload :LogSubscriptionHandler, 'logcraft/rails/log_subscription_handler'
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rails/railtie'
|
4
|
+
|
5
|
+
module Logcraft
|
6
|
+
class Railtie < ::Rails::Railtie
|
7
|
+
config.logcraft = ActiveSupport::OrderedOptions.new
|
8
|
+
config.logcraft.initial_context = {}
|
9
|
+
config.logcraft.layout_options = {}
|
10
|
+
|
11
|
+
config.logcraft.access_log = ActiveSupport::OrderedOptions.new
|
12
|
+
config.logcraft.access_log.logger_name = 'AccessLog'
|
13
|
+
config.logcraft.access_log.exclude_paths = []
|
14
|
+
config.logcraft.access_log.log_only_whitelisted_params = false
|
15
|
+
config.logcraft.access_log.whitelisted_params = [:controller, :action]
|
16
|
+
|
17
|
+
initializer 'logcraft.initialize' do |app|
|
18
|
+
Logcraft.initialize log_level: app.config.log_level,
|
19
|
+
initial_context: app.config.logcraft.initial_context,
|
20
|
+
layout_options: app.config.logcraft.layout_options
|
21
|
+
end
|
22
|
+
|
23
|
+
initializer 'logcraft.configure_rails' do |app|
|
24
|
+
require 'logcraft/rails/extensions'
|
25
|
+
app.config.middleware.insert_before ::Rails::Rack::Logger,
|
26
|
+
Logcraft::Rails::RequestLogger,
|
27
|
+
Logcraft.logger(config.logcraft.access_log.logger_name),
|
28
|
+
config.logcraft.access_log
|
29
|
+
app.config.middleware.delete ::Rails::Rack::Logger
|
30
|
+
app.config.middleware.insert_after ::ActionDispatch::RequestId, Logcraft::Rails::RequestIdLogger
|
31
|
+
end
|
32
|
+
|
33
|
+
config.after_initialize do
|
34
|
+
Logcraft::Rails::LogSubscriptionHandler.detach ::ActionController::LogSubscriber, :action_controller
|
35
|
+
require 'action_view/log_subscriber' unless defined? ::ActionView::LogSubscriber
|
36
|
+
Logcraft::Rails::LogSubscriptionHandler.detach ::ActionView::LogSubscriber, :action_view
|
37
|
+
if defined? ::ActiveRecord
|
38
|
+
Logcraft::Rails::LogSubscriptionHandler.detach ::ActiveRecord::LogSubscriber, :active_record
|
39
|
+
Logcraft::Rails::LogSubscriptionHandler.attach Logcraft::Rails::ActiveRecord::LogSubscriber, :active_record
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
config.before_configuration do |app|
|
44
|
+
app.config.logger = Logcraft.logger 'Application'
|
45
|
+
app.config.log_level = ENV['LOG_LEVEL'] || :info
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
|
5
|
+
RSpec::Matchers.define :include_log_message do |expected|
|
6
|
+
chain :at_level, :log_level
|
7
|
+
|
8
|
+
match do |actual|
|
9
|
+
actual.any? { |log_line| includes? log_line, expected_messages_from(expected) }
|
10
|
+
end
|
11
|
+
|
12
|
+
def includes?(log_line, messages)
|
13
|
+
return false unless includes_log_level? log_line
|
14
|
+
messages.all? { |message| log_line.include? message }
|
15
|
+
end
|
16
|
+
|
17
|
+
def includes_log_level?(log_line)
|
18
|
+
return true if log_level.nil?
|
19
|
+
log_line.include? log_level_string(log_level)
|
20
|
+
end
|
21
|
+
|
22
|
+
def log_level_string(log_level)
|
23
|
+
return 'WARN' if log_level == :warning
|
24
|
+
log_level.to_s.upcase
|
25
|
+
end
|
26
|
+
|
27
|
+
def expected_messages_from(object)
|
28
|
+
@expected_messages ||= case object
|
29
|
+
when Hash
|
30
|
+
object.map { |k, v| JSON.dump(k => v)[1...-1] }
|
31
|
+
when String
|
32
|
+
[object]
|
33
|
+
else
|
34
|
+
raise NotImplementedError, 'log expectation must be Hash or String'
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
failure_message do |actual|
|
39
|
+
error_message = "expected log output\n\t'#{actual.join('')}'\nto include log message\n\t'#{expected}'"
|
40
|
+
error_message += " at #{log_level} level" if log_level
|
41
|
+
error_message
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
RSpec::Matchers.define :log do
|
46
|
+
supports_block_expectations
|
47
|
+
chain :at_level, :log_level
|
48
|
+
|
49
|
+
failure_message do
|
50
|
+
error_message = "expected operation to log '#{expected}'"
|
51
|
+
error_message += " at #{log_level} level" if log_level
|
52
|
+
"#{error_message}\n\nactual log output:\n#{log_output.join('')}"
|
53
|
+
end
|
54
|
+
|
55
|
+
match do |operation|
|
56
|
+
raise 'log matcher only supports block expectations' unless operation.is_a? Proc
|
57
|
+
log_output_is_expected.not_to include_log_message(expected)
|
58
|
+
operation.call
|
59
|
+
log_output_is_expected.to include_log_message(expected).at_level(log_level)
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'logging'
|
4
|
+
require 'rspec/logging_helper'
|
5
|
+
require_relative 'rspec/helpers'
|
6
|
+
require_relative 'rspec/matchers'
|
7
|
+
require_relative 'log_layout'
|
8
|
+
|
9
|
+
RSpec.configure do |config|
|
10
|
+
config.include Ezlog::RSpec::Helpers
|
11
|
+
config.before(:suite) do
|
12
|
+
Logging.appenders.string_io('__logcraft_stringio__', layout: Logging.logger.root.appenders.first&.layout || Logcraft::LogLayout.new)
|
13
|
+
config.capture_log_messages to: '__logcraft_stringio__'
|
14
|
+
end
|
15
|
+
end
|
data/lib/logcraft.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'logging'
|
4
|
+
|
5
|
+
require 'logcraft/version'
|
6
|
+
require 'logcraft/railtie' if defined? Rails
|
7
|
+
|
8
|
+
module Logcraft
|
9
|
+
autoload :LogContextHelper, 'logcraft/log_context_helper'
|
10
|
+
autoload :LogLayout, 'logcraft/log_layout'
|
11
|
+
autoload :Rails, 'logcraft/rails'
|
12
|
+
|
13
|
+
extend LogContextHelper
|
14
|
+
|
15
|
+
def self.initialize(log_level: :info, initial_context: {}, layout_options: {})
|
16
|
+
Logging.logger.root.appenders = Logging.appenders.stdout layout: LogLayout.new(initial_context, layout_options)
|
17
|
+
Logging.logger.root.level = log_level
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.logger(name, level = nil)
|
21
|
+
Logging::Logger[name].tap { |logger| logger.level = level if level }
|
22
|
+
end
|
23
|
+
end
|
data/logcraft.gemspec
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "lib/logcraft/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = "logcraft"
|
7
|
+
spec.version = Logcraft::VERSION
|
8
|
+
spec.authors = ["Zoltan Ormandi"]
|
9
|
+
spec.email = ["zoltan.ormandi@gmail.com"]
|
10
|
+
|
11
|
+
spec.summary = "A zero-configuration logging solution for Ruby on Rails."
|
12
|
+
spec.homepage = "https://github.com/zormandi/logcraft"
|
13
|
+
spec.license = "MIT"
|
14
|
+
spec.required_ruby_version = ">= 2.6.0"
|
15
|
+
|
16
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
17
|
+
spec.metadata["source_code_uri"] = "https://github.com/zormandi/logcraft"
|
18
|
+
spec.metadata["changelog_uri"] = "https://github.com/zormandi/logcraft/blob/master/CHANGELOG.md"
|
19
|
+
|
20
|
+
# Specify which files should be added to the gem when it is released.
|
21
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
22
|
+
spec.files = Dir.chdir(__dir__) do
|
23
|
+
`git ls-files -z`.split("\x0").reject do |f|
|
24
|
+
(f == __FILE__) || f.match(%r{\A(?:(?:bin|test|spec|features)/|\.(?:git|travis|circleci)|appveyor)})
|
25
|
+
end
|
26
|
+
end
|
27
|
+
spec.bindir = "exe"
|
28
|
+
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
29
|
+
spec.require_paths = ["lib"]
|
30
|
+
|
31
|
+
spec.add_dependency "logging", "~> 2.0"
|
32
|
+
spec.add_dependency "multi_json", "~> 1.14"
|
33
|
+
|
34
|
+
spec.add_development_dependency "bundler", "~> 2.0"
|
35
|
+
spec.add_development_dependency "rake", ">= 12.0"
|
36
|
+
spec.add_development_dependency "rspec", "~> 3.0"
|
37
|
+
end
|
metadata
ADDED
@@ -0,0 +1,142 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: logcraft
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0.rc
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Zoltan Ormandi
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2022-06-26 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: logging
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '2.0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '2.0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: multi_json
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1.14'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1.14'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: bundler
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '2.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '2.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rake
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '12.0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '12.0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rspec
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '3.0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '3.0'
|
83
|
+
description:
|
84
|
+
email:
|
85
|
+
- zoltan.ormandi@gmail.com
|
86
|
+
executables: []
|
87
|
+
extensions: []
|
88
|
+
extra_rdoc_files: []
|
89
|
+
files:
|
90
|
+
- ".rspec"
|
91
|
+
- CHANGELOG.md
|
92
|
+
- CODE_OF_CONDUCT.md
|
93
|
+
- Gemfile
|
94
|
+
- LICENSE.txt
|
95
|
+
- README.md
|
96
|
+
- Rakefile
|
97
|
+
- gemfiles/rails_5.2.gemfile
|
98
|
+
- gemfiles/rails_6.0.gemfile
|
99
|
+
- gemfiles/rails_6.1.gemfile
|
100
|
+
- lib/logcraft.rb
|
101
|
+
- lib/logcraft/log_context_helper.rb
|
102
|
+
- lib/logcraft/log_layout.rb
|
103
|
+
- lib/logcraft/rails.rb
|
104
|
+
- lib/logcraft/rails/active_record.rb
|
105
|
+
- lib/logcraft/rails/active_record/log_subscriber.rb
|
106
|
+
- lib/logcraft/rails/extensions.rb
|
107
|
+
- lib/logcraft/rails/log_subscription_handler.rb
|
108
|
+
- lib/logcraft/rails/request_id_logger.rb
|
109
|
+
- lib/logcraft/rails/request_logger.rb
|
110
|
+
- lib/logcraft/railtie.rb
|
111
|
+
- lib/logcraft/rspec.rb
|
112
|
+
- lib/logcraft/rspec/helpers.rb
|
113
|
+
- lib/logcraft/rspec/matchers.rb
|
114
|
+
- lib/logcraft/version.rb
|
115
|
+
- logcraft.gemspec
|
116
|
+
homepage: https://github.com/zormandi/logcraft
|
117
|
+
licenses:
|
118
|
+
- MIT
|
119
|
+
metadata:
|
120
|
+
homepage_uri: https://github.com/zormandi/logcraft
|
121
|
+
source_code_uri: https://github.com/zormandi/logcraft
|
122
|
+
changelog_uri: https://github.com/zormandi/logcraft/blob/master/CHANGELOG.md
|
123
|
+
post_install_message:
|
124
|
+
rdoc_options: []
|
125
|
+
require_paths:
|
126
|
+
- lib
|
127
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - ">="
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: 2.6.0
|
132
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
133
|
+
requirements:
|
134
|
+
- - ">"
|
135
|
+
- !ruby/object:Gem::Version
|
136
|
+
version: 1.3.1
|
137
|
+
requirements: []
|
138
|
+
rubygems_version: 3.1.6
|
139
|
+
signing_key:
|
140
|
+
specification_version: 4
|
141
|
+
summary: A zero-configuration logging solution for Ruby on Rails.
|
142
|
+
test_files: []
|