loga 1.4.0 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +21 -9
- data/CHANGELOG.md +50 -0
- data/Guardfile +45 -0
- data/LICENSE.txt +29 -0
- data/README.md +149 -81
- data/circle.yml +5 -5
- data/lib/loga.rb +12 -8
- data/lib/loga/configuration.rb +97 -46
- data/lib/loga/event.rb +11 -0
- data/lib/loga/rack/logger.rb +21 -16
- data/lib/loga/rack/request.rb +18 -6
- data/lib/loga/railtie.rb +74 -33
- data/lib/loga/service_version_strategies.rb +19 -0
- data/lib/loga/version.rb +1 -1
- data/loga.gemspec +5 -1
- data/spec/fixtures/rails32/config/application.rb +6 -6
- data/spec/fixtures/rails40/config/application.rb +6 -6
- data/spec/fixtures/rails50/config/application.rb +6 -6
- data/spec/integration/rails/railtie_spec.rb +69 -61
- data/spec/integration/rails/request_spec.rb +17 -18
- data/spec/integration/sinatra_spec.rb +53 -14
- data/spec/support/request_spec.rb +11 -10
- data/spec/unit/loga/configuration_spec.rb +163 -57
- data/spec/unit/loga/event_spec.rb +31 -1
- data/spec/unit/loga/rack/logger_spec.rb +22 -10
- data/spec/unit/loga/rack/request_spec.rb +19 -26
- data/spec/unit/loga/service_version_strategies_spec.rb +37 -0
- data/spec/unit/loga_spec.rb +52 -15
- metadata +53 -8
- data/.rubocop_todo.yml +0 -33
- data/lib/loga/revision_strategy.rb +0 -32
- data/spec/unit/loga/revision_strategy_spec.rb +0 -41
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 58717f46fed6f5a99834472829baef870624e67d
|
4
|
+
data.tar.gz: efbaf0fcdb8c50a0d5193ecd1be1afcc03d4fa19
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 62433f9172394016bc78a9b49a6c42f90827e925e1b5b5e9704367782705f821c69ff43afc5feb7a25f1eb87ff4a9e4084ad4d5b1e264d19a0c0a4e3feb1b9d7
|
7
|
+
data.tar.gz: fe2281a9a05f805402aab31da3add62067a718e58c0cc7c28bd6731e587301b7cd9dd5f4d86672303b5e93d332ec9a6acc1e11ff38db7ff288774dfd76b668b4
|
data/.rubocop.yml
CHANGED
@@ -1,17 +1,24 @@
|
|
1
|
-
inherit_from: .rubocop_todo.yml
|
2
|
-
|
3
1
|
AllCops:
|
4
2
|
Exclude:
|
5
3
|
- 'spec/fixtures/**/*'
|
6
4
|
- '*.gemspec'
|
7
5
|
|
8
|
-
|
6
|
+
Documentation:
|
7
|
+
Enabled: false
|
8
|
+
|
9
|
+
Metrics/AbcSize:
|
10
|
+
Enabled: false
|
11
|
+
|
12
|
+
Metrics/LineLength:
|
9
13
|
Enabled: true
|
10
|
-
|
14
|
+
Max: 90
|
11
15
|
|
12
|
-
|
16
|
+
Metrics/MethodLength:
|
13
17
|
Enabled: true
|
14
|
-
|
18
|
+
Max: 15
|
19
|
+
|
20
|
+
Style/ExtraSpacing:
|
21
|
+
Enabled: false
|
15
22
|
|
16
23
|
Style/BlockDelimiters:
|
17
24
|
Enabled: false
|
@@ -22,8 +29,13 @@ Style/FormatString:
|
|
22
29
|
Style/PerlBackrefs:
|
23
30
|
Enabled: false
|
24
31
|
|
25
|
-
Style/ExtraSpacing:
|
26
|
-
Enabled: false
|
27
|
-
|
28
32
|
Style/SpaceAroundOperators:
|
29
33
|
Enabled: false
|
34
|
+
|
35
|
+
Style/TrailingCommaInLiteral:
|
36
|
+
Enabled: true
|
37
|
+
EnforcedStyleForMultiline: comma
|
38
|
+
|
39
|
+
Style/TrailingCommaInArguments:
|
40
|
+
Enabled: true
|
41
|
+
EnforcedStyleForMultiline: comma
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
# Change Log
|
2
|
+
All notable changes to this project will be documented in this file.
|
3
|
+
|
4
|
+
The format is based on [Keep a Changelog](http://keepachangelog.com/)
|
5
|
+
and this project adheres to [Semantic Versioning](http://semver.org/).
|
6
|
+
|
7
|
+
## [2.0.0]
|
8
|
+
## [2.0.0.pre.3]
|
9
|
+
## [2.0.0.pre.2]
|
10
|
+
## [2.0.0.pre1]
|
11
|
+
### Added
|
12
|
+
- Human readable formatter `SimpleFormatter`
|
13
|
+
- `LOGA_FORMAT` environment variable to switch between (gelf|simple) formatters
|
14
|
+
- Added `format` and `filter_exceptions` configuration options
|
15
|
+
|
16
|
+
### Changed
|
17
|
+
#### Configuration interface
|
18
|
+
- Configure via Hash instead of Block
|
19
|
+
- String only `service_version` configuration option
|
20
|
+
|
21
|
+
#### Rails
|
22
|
+
- Use Loga everywhere with environment based configuration
|
23
|
+
- Added `ActiveRecord::RecordNotFound` to default `filter_exceptions`
|
24
|
+
- Removed `enabled` and `silence_rails_rack_logger` configure options
|
25
|
+
- Enforce Rails configuration options over Loga where possible
|
26
|
+
|
27
|
+
#### Sinatra
|
28
|
+
- Removed logger and tags parameters in `Loga::Rack::Logger`
|
29
|
+
|
30
|
+
### Fixed
|
31
|
+
- Uninitialized `Loga.logger` in Rails
|
32
|
+
|
33
|
+
## [1.4.0] - 2016-09-13
|
34
|
+
### Added
|
35
|
+
- Rails 5 support
|
36
|
+
- Silence ActionController::LogSubscriber
|
37
|
+
|
38
|
+
### Changed
|
39
|
+
- Update GELF payload to include `_request.controller` when available (Rails Controller/Action)
|
40
|
+
|
41
|
+
## [1.3.0] - 2016-09-07
|
42
|
+
### Changed
|
43
|
+
- Silence ActionDispatch::DebugExceptions' logger
|
44
|
+
|
45
|
+
[2.0.0]: https://github.com/FundingCircle/loga/compare/v2.0.0.pre.3...v2.0.0
|
46
|
+
[2.0.0.pre.3]: https://github.com/FundingCircle/loga/compare/v2.0.0.pre.2...v2.0.0.pre.3
|
47
|
+
[2.0.0.pre.2]: https://github.com/FundingCircle/loga/compare/v2.0.0.pre1...v2.0.0.pre.2
|
48
|
+
[2.0.0.pre1]: https://github.com/FundingCircle/loga/compare/v1.4.0...v2.0.0.pre1
|
49
|
+
[1.4.0]: https://github.com/FundingCircle/loga/compare/v1.3.0...v1.4.0
|
50
|
+
[1.3.0]: https://github.com/FundingCircle/loga/compare/v1.2.1...v1.3.0
|
data/Guardfile
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
guard :rubocop do
|
2
|
+
watch(%r{^lib/.+\.rb$})
|
3
|
+
watch(%r{^spec/.+\.rb$})
|
4
|
+
end
|
5
|
+
|
6
|
+
group :sinatra do
|
7
|
+
%w(production development).each do |env|
|
8
|
+
guard :rspec,
|
9
|
+
all_on_start: true,
|
10
|
+
cmd: "RACK_ENV=#{env} bundle exec appraisal sinatra14 rspec" do
|
11
|
+
watch(%r{^spec/integration/sinatra_spec.rb$})
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
group :rails do
|
17
|
+
%w(production development).each do |env|
|
18
|
+
%w(rails32 rails40 rails50).each do |appraisal|
|
19
|
+
guard :rspec,
|
20
|
+
all_on_start: true,
|
21
|
+
cmd: "RACK_ENV=#{env} bundle exec appraisal #{appraisal} rspec" do
|
22
|
+
watch('lib/loga/railtie.rb') do
|
23
|
+
[
|
24
|
+
'spec/integration/rails/request_spec.rb',
|
25
|
+
'spec/integration/rails/railtie_spec.rb',
|
26
|
+
]
|
27
|
+
end
|
28
|
+
watch(%r{^spec/fixtures/rails\d{2}/.+\.rb$}) do
|
29
|
+
[
|
30
|
+
'spec/integration/rails/request_spec.rb',
|
31
|
+
'spec/integration/rails/railtie_spec.rb',
|
32
|
+
]
|
33
|
+
end
|
34
|
+
watch(%r{^spec/integration/rails/.+_spec\.rb$})
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
group :unit do
|
41
|
+
guard :rspec, cmd: 'bundle exec appraisal unit rspec' do
|
42
|
+
watch(%r{^spec/unit/.+_spec\.rb$})
|
43
|
+
watch(%r{^lib/(.+)\.rb$}) { |m| "spec/unit/#{m[1]}_spec.rb" }
|
44
|
+
end
|
45
|
+
end
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
Copyright (c) 2015, Funding Circle Ltd.
|
2
|
+
All rights reserved.
|
3
|
+
|
4
|
+
Redistribution and use in source and binary forms, with or without
|
5
|
+
modification, are permitted provided that the following conditions are met:
|
6
|
+
|
7
|
+
1. Redistributions of source code must retain the above copyright
|
8
|
+
notice, this list of conditions and the following disclaimer.
|
9
|
+
|
10
|
+
2. Redistributions in binary form must reproduce the above copyright
|
11
|
+
notice, this list of conditions and the following disclaimer in the
|
12
|
+
documentation and/or other materials provided with the distribution.
|
13
|
+
|
14
|
+
3. Neither the name of the copyright holder nor the names of its
|
15
|
+
contributors may be used to endorse or promote products derived from
|
16
|
+
this software without specific prior written permission.
|
17
|
+
|
18
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
|
19
|
+
IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
|
20
|
+
TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
|
21
|
+
PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
22
|
+
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
23
|
+
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
|
24
|
+
TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
25
|
+
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
26
|
+
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
27
|
+
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
28
|
+
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
29
|
+
|
data/README.md
CHANGED
@@ -2,146 +2,214 @@
|
|
2
2
|
|
3
3
|
## Description
|
4
4
|
|
5
|
-
Loga
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
-
|
10
|
-
-
|
5
|
+
Loga provides consistent logging across frameworks and environments.
|
6
|
+
|
7
|
+
Includes:
|
8
|
+
- One logger for all environments
|
9
|
+
- Human readable logs for development
|
10
|
+
- Structured logs for production ([GELF](http://docs.graylog.org/en/2.1/pages/gelf.html))
|
11
|
+
- One Rack logger for all Rack based applications
|
12
|
+
|
13
|
+
## TOC
|
14
|
+
|
15
|
+
- [Installation](#installation)
|
16
|
+
- [Rails](#rails)
|
17
|
+
- [Reduced logs](#reduced-logs)
|
18
|
+
- [Request log tags](#request-log-tags)
|
19
|
+
- [Sinatra](#sinatra)
|
20
|
+
- [GELF Output example](#gelf-output-example)
|
21
|
+
- [Road map](#road-map)
|
22
|
+
- [Contributing](#contributing)
|
23
|
+
- [Running tests](#running-tests)
|
24
|
+
- [Credits](#credits)
|
25
|
+
- [License](#license)
|
11
26
|
|
12
27
|
## Installation
|
13
28
|
|
14
29
|
Add this line to your application's Gemfile:
|
15
30
|
|
16
|
-
|
31
|
+
```
|
32
|
+
gem 'loga', git: 'git@github.com:FundingCircle/loga.git'
|
33
|
+
```
|
34
|
+
|
35
|
+
### Rails
|
17
36
|
|
18
|
-
|
37
|
+
Let Loga know what your application name is and Loga will do the rest.
|
19
38
|
|
20
|
-
|
39
|
+
```ruby
|
40
|
+
# config/application.rb
|
41
|
+
class MyApp::Application < Rails::Application
|
42
|
+
config.loga = { service_name: 'MyApp' }
|
43
|
+
end
|
44
|
+
```
|
21
45
|
|
22
|
-
|
46
|
+
Loga hooks into the Rails logger initialization process and defines its own logger for all environments.
|
23
47
|
|
24
|
-
|
25
|
-
using plain Ruby.
|
48
|
+
The logger configuration adjusts based on the environment:
|
26
49
|
|
27
|
-
|
50
|
+
| | Production | Test | Development | Others |
|
51
|
+
|--------|------------|--------------|-------------|--------|
|
52
|
+
| Output | STDOUT | log/test.log | STDOUT | STDOUT |
|
53
|
+
| Format | gelf | simple | simple | simple |
|
28
54
|
|
29
|
-
|
30
|
-
the Railtie.
|
55
|
+
You can customize the configuration to your liking:
|
31
56
|
|
32
57
|
```ruby
|
33
|
-
# config/
|
34
|
-
|
35
|
-
config.loga
|
36
|
-
|
58
|
+
# config/application.rb
|
59
|
+
class MyApp::Application < Rails::Application
|
60
|
+
config.loga = {
|
61
|
+
device: File.open("log/application.log", 'a'),
|
62
|
+
format: :gelf,
|
63
|
+
service_name: 'MyApp',
|
64
|
+
}
|
37
65
|
end
|
38
|
-
...
|
39
66
|
```
|
40
67
|
|
41
|
-
|
68
|
+
Loga leverages existing Rails configuration options:
|
42
69
|
|
43
|
-
|
70
|
+
- `config.filter_parameters`
|
71
|
+
- `config.log_level`
|
72
|
+
- `config.log_tags`
|
44
73
|
|
45
|
-
|
46
|
-
# .../initializers/loga.rb
|
47
|
-
require 'loga'
|
74
|
+
Use these options to customize Loga instead of the Loga options hash.
|
48
75
|
|
49
|
-
Loga.
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
```
|
54
|
-
Log requests in Rack applications with Loga middleware.
|
76
|
+
Inside your application use `Rails.logger` instead of `Loga.logger`, even though
|
77
|
+
they are equivalent, to prevent lock-in.
|
78
|
+
|
79
|
+
#### Reduced logs
|
55
80
|
|
56
|
-
|
81
|
+
When the format set to `gelf` requests logs are reduced to a single log entry, which
|
82
|
+
could include an exception.
|
83
|
+
|
84
|
+
This is made possible by silencing these loggers:
|
85
|
+
|
86
|
+
- `Rack::Request::Logger`
|
87
|
+
- `ActionDispatch::DebugExceptions`
|
88
|
+
- `ActionController::LogSubscriber`
|
89
|
+
- `ActionView::LogSubscriber`
|
90
|
+
|
91
|
+
#### Request log tags
|
92
|
+
|
93
|
+
To provide consistency between Rails and other Rack frameworks, tags (e.i `config.log_tags`)
|
94
|
+
are computed with a [Loga::Rack::Request](lib/loga/rack/request.rb) as
|
95
|
+
opposed to a `ActionDispatch::Request`.
|
96
|
+
|
97
|
+
### Sinatra
|
98
|
+
|
99
|
+
With Sinatra Loga needs to be configured manually:
|
57
100
|
|
58
101
|
```ruby
|
59
|
-
|
102
|
+
require 'loga'
|
103
|
+
|
104
|
+
Loga.configure(
|
105
|
+
filter_parameters: [:password],
|
106
|
+
format: :gelf,
|
107
|
+
service_name: 'my_app',
|
108
|
+
tags: [:uuid],
|
109
|
+
)
|
110
|
+
|
60
111
|
use Loga::Rack::RequestId
|
61
|
-
use Loga::Rack::Logger
|
112
|
+
use Loga::Rack::Logger
|
62
113
|
|
63
|
-
|
114
|
+
use MyApp
|
64
115
|
run Sinatra::Application
|
65
116
|
```
|
66
117
|
|
67
|
-
|
118
|
+
You can now use `Loga.logger` or assign it to your existing logger.
|
119
|
+
The above configuration also inserts two middleware:
|
68
120
|
|
69
|
-
|
70
|
-
|
71
|
-
| host | String | nil | Service hostname. When nil the hostname is computed with `Socket.gethostname` |
|
72
|
-
| service_version | String/Symbol | :git | Service version is embedded in every message. When Symbol the version is computed with a strategy. |
|
73
|
-
| service_name | String | nil | Service name is embedded in every message |
|
74
|
-
| device | IO | nil | The device the logger writes to |
|
75
|
-
| sync | Boolean | true | Sync IO |
|
76
|
-
| level | Symbol | :info | The level to logger logs at |
|
77
|
-
| enabled | Boolean | true | Enable/Disable Loga in Rails |
|
121
|
+
- `Loga::Rack::RequestId` makes the request id available to the request logger
|
122
|
+
- `Loga::Rack::Logger` logs requests
|
78
123
|
|
79
|
-
##
|
124
|
+
## GELF Output Example
|
125
|
+
|
126
|
+
Rails request logger: (includes controller/action name):
|
127
|
+
|
128
|
+
`GET /ok`
|
80
129
|
|
81
|
-
```ruby
|
82
|
-
# Anywhere in your application
|
83
|
-
Loga.logger.info('Hello World')
|
84
|
-
```
|
85
130
|
```json
|
86
|
-
//GELF Output
|
87
131
|
{
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
132
|
+
"_request.status": 200,
|
133
|
+
"_request.method": "GET",
|
134
|
+
"_request.path": "/ok",
|
135
|
+
"_request.params": {},
|
136
|
+
"_request.request_id": "2b99e3d3-3ee2-4781-972b-782682f57648",
|
137
|
+
"_request.request_ip": "127.0.0.1",
|
138
|
+
"_request.user_agent": null,
|
139
|
+
"_request.controller": "ApplicationController#ok",
|
140
|
+
"_request.duration": 0,
|
141
|
+
"_type": "request",
|
142
|
+
"_service.name": "my_app",
|
143
|
+
"_service.version": "1.0",
|
144
|
+
"_tags": "2b99e3d3-3ee2-4781-972b-782682f57648",
|
145
|
+
"short_message": "GET /ok 200 in 0ms",
|
146
|
+
"timestamp": 1450150205.123,
|
147
|
+
"host": "example.com",
|
148
|
+
"level": 6,
|
149
|
+
"version": "1.1"
|
96
150
|
}
|
97
151
|
```
|
98
152
|
|
99
|
-
|
153
|
+
Sinatra request output is identical to Rails but without the `_request.controller` key.
|
100
154
|
|
101
|
-
|
155
|
+
Logger output:
|
102
156
|
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
- Loga formats timestamps in seconds since UNIX epoch with 3 decimal places
|
110
|
-
for milliseconds. Which is in accordance with GELF 1.1 specification.
|
157
|
+
```ruby
|
158
|
+
Rails.logger.info('I love Loga')
|
159
|
+
# or
|
160
|
+
Loga.logger.info('I love Loga')
|
161
|
+
```
|
111
162
|
|
163
|
+
```json
|
164
|
+
{
|
165
|
+
"_service.name": "my_app",
|
166
|
+
"_service.version": "v1.0.0",
|
167
|
+
"_tags": "",
|
168
|
+
"host": "example.com",
|
169
|
+
"level": 6,
|
170
|
+
"short_message": "I love Loga",
|
171
|
+
"timestamp": 1450150205.123,
|
172
|
+
"version": "1.1"
|
173
|
+
}
|
174
|
+
```
|
112
175
|
|
113
|
-
## Road
|
176
|
+
## Road map
|
114
177
|
|
115
178
|
Consult the [milestones](https://github.com/FundingCircle/loga/milestones).
|
116
179
|
|
117
180
|
## Contributing
|
118
181
|
|
119
|
-
|
120
|
-
|
121
|
-
1. Fork it ( https://github.com/FundingCircle/loga/fork )
|
122
|
-
2. Create your feature branch (`git checkout -b my-new-feature`)
|
123
|
-
3. Commit your changes (`git commit -am 'Add some feature'`)
|
124
|
-
4. Push to the branch (`git push origin my-new-feature`)
|
125
|
-
5. Create a new Pull Request
|
182
|
+
Loga is in active development, feedback and contributions are welcomed.
|
126
183
|
|
127
184
|
### Running tests
|
128
185
|
|
129
|
-
This project uses [`appraisal`](https://github.com/thoughtbot/appraisal/tree/v2.0.2)
|
186
|
+
This project uses [`appraisal`](https://github.com/thoughtbot/appraisal/tree/v2.0.2)
|
187
|
+
to run tests against different versions of dependencies (e.g. Rails, Sinatra).
|
130
188
|
|
131
|
-
|
189
|
+
Install Loga dependencies with `bundle install` and then appraisals
|
190
|
+
with `bundle exec appraisal install`.
|
132
191
|
|
133
192
|
Run all tests with `bundle exec appraisal rspec`.
|
134
193
|
|
135
194
|
You can run tests for one appraisal with `bundle exec appraisal appraisal-name rspec`.
|
195
|
+
Refer to the [Appraisals](Appraisals) file for a complete lists of appraisals.
|
196
|
+
|
197
|
+
Prefix test command with RACK\_ENV to switch between environments for Rack based tests
|
198
|
+
`RACK_ENV=production bundle exec appraisal rspec`.
|
136
199
|
|
137
|
-
|
200
|
+
Experiment Guard support introduced to ease running tests locally `bundle exec guard`.
|
138
201
|
|
139
|
-
|
202
|
+
[CI](https://circleci.com/gh/FundingCircle/loga) results are the source of truth.
|
140
203
|
|
141
204
|
## Credits
|
142
205
|
|
206
|
+
- [Lograge](https://github.com/roidrage/lograge)
|
143
207
|
- [LogStashLogger](https://github.com/dwbutler/logstash-logger)
|
144
208
|
- [Rails](https://github.com/rails/rails)
|
145
209
|
- [RackLogstasher](https://github.com/alphagov/rack-logstasher)
|
146
210
|
|
211
|
+
## License
|
212
|
+
|
147
213
|
Copyright (c) 2015 Funding Circle. All rights reserved.
|
214
|
+
|
215
|
+
Distributed under the BSD 3-Clause License.
|