pliny 0.23.0 → 0.24.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 +4 -4
- data/lib/pliny.rb +2 -0
- data/lib/pliny/canonical_log_line_helpers.rb +46 -0
- data/lib/pliny/log.rb +4 -0
- data/lib/pliny/middleware/canonical_log_line.rb +125 -0
- data/lib/pliny/middleware/rescue_errors.rb +8 -0
- data/lib/pliny/version.rb +1 -1
- data/lib/template/Gemfile +1 -1
- data/lib/template/lib/routes.rb +4 -1
- data/spec/canonical_log_line_helpers_spec.rb +39 -0
- data/spec/log_spec.rb +6 -0
- data/spec/middleware/canonical_log_line_spec.rb +88 -0
- metadata +11 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6b59272bbe834eaf737b9ffe3e4769e54f8897cf
|
4
|
+
data.tar.gz: 13bfa08975ac6f65c74a686e54cf22608bbab639
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1d76b1d6abaf678044111133e3fcb4aa587b88e87c866d8e60adbc03543e77f6e1cb959609eea9b8bb75931ea6f206c49c8447852b4035ca00a6360bd4408e0c
|
7
|
+
data.tar.gz: 8a4e54331f8642b58176358fb3d57845cf623dc93a3d8e0bdd301ea853a84ee29db9c2d1dc5567b6db6cf3fc5f920d8348b419a09f0b3a4ae49593597110b4d6
|
data/lib/pliny.rb
CHANGED
@@ -2,6 +2,7 @@ require "multi_json"
|
|
2
2
|
require "sinatra/base"
|
3
3
|
|
4
4
|
require_relative "pliny/version"
|
5
|
+
require_relative "pliny/canonical_log_line_helpers"
|
5
6
|
require_relative "pliny/error_reporters"
|
6
7
|
require_relative "pliny/errors"
|
7
8
|
require_relative "pliny/helpers/encode"
|
@@ -14,6 +15,7 @@ require_relative "pliny/request_store"
|
|
14
15
|
require_relative "pliny/rollbar_logger"
|
15
16
|
require_relative "pliny/router"
|
16
17
|
require_relative "pliny/utils"
|
18
|
+
require_relative "pliny/middleware/canonical_log_line"
|
17
19
|
require_relative "pliny/middleware/cors"
|
18
20
|
require_relative "pliny/middleware/instruments"
|
19
21
|
require_relative "pliny/middleware/metrics"
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module Pliny
|
2
|
+
# Helpers to produce a canonical log line. This mostly amounts to a set of
|
3
|
+
# accessors that do basic type checking combined with tracking an internal
|
4
|
+
# schema so that we can produce a hash of everything that's been set so far.
|
5
|
+
module CanonicalLogLineHelpers
|
6
|
+
module ClassMethods
|
7
|
+
def log_field(name, type)
|
8
|
+
unless name.is_a?(Symbol)
|
9
|
+
raise ArgumentError, "Expected first argument to be a symbol"
|
10
|
+
end
|
11
|
+
|
12
|
+
@fields ||= {}
|
13
|
+
@fields[name] = type
|
14
|
+
define_method(:"#{name}=") do |val|
|
15
|
+
set_field(name, val)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.included(klass)
|
21
|
+
klass.extend(ClassMethods)
|
22
|
+
end
|
23
|
+
|
24
|
+
def to_h
|
25
|
+
@values
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def set_field(name, value)
|
31
|
+
type = self.class.instance_variable_get(:@fields)[name]
|
32
|
+
|
33
|
+
unless type
|
34
|
+
raise ArgumentError, "Field #{name} undefined"
|
35
|
+
end
|
36
|
+
|
37
|
+
if !value.nil? && !value.is_a?(type)
|
38
|
+
raise ArgumentError,
|
39
|
+
"Expected #{name} to be type #{type} (was #{value.class.name})"
|
40
|
+
end
|
41
|
+
|
42
|
+
@values ||= {}
|
43
|
+
@values[name] = value
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
data/lib/pliny/log.rb
CHANGED
@@ -0,0 +1,125 @@
|
|
1
|
+
module Pliny::Middleware
|
2
|
+
# Emits a "canonical log line", i.e. a single log line that contains as much
|
3
|
+
# relevant information about a request as possible and which makes for a
|
4
|
+
# single convenient reference point to understand all the vitals of any
|
5
|
+
# single request.
|
6
|
+
#
|
7
|
+
# This default implementation contains some useful data to get a project
|
8
|
+
# started, but it's usually recommended to vendor this middleware into your
|
9
|
+
# project and start adding some custom fields. Some examples of those might
|
10
|
+
# be:
|
11
|
+
#
|
12
|
+
# * ID and email of an authenticated user.
|
13
|
+
# * ID of API key used, OAuth application and scope.
|
14
|
+
# * Remaining and total rate limits.
|
15
|
+
# * Name of the service, HEAD revision, release number.
|
16
|
+
# * Name of the internal system that initiated the request.
|
17
|
+
#
|
18
|
+
class CanonicalLogLine
|
19
|
+
# LogLine is a nested model that allows us to construct a canonical log
|
20
|
+
# line in a way that's reasonably well organized and somewhat type safe.
|
21
|
+
# It's responsible for hashifying its defined fields for emission into a
|
22
|
+
# log stream or elsewhere.
|
23
|
+
class LogLine
|
24
|
+
include Pliny::CanonicalLogLineHelpers
|
25
|
+
|
26
|
+
#
|
27
|
+
# error
|
28
|
+
#
|
29
|
+
|
30
|
+
log_field :error_class, String
|
31
|
+
log_field :error_id, String
|
32
|
+
log_field :error_message, String
|
33
|
+
|
34
|
+
#
|
35
|
+
# request
|
36
|
+
#
|
37
|
+
|
38
|
+
log_field :request_id, String
|
39
|
+
log_field :request_method, String
|
40
|
+
log_field :request_ip, String
|
41
|
+
log_field :request_path, String
|
42
|
+
log_field :request_route_signature, String
|
43
|
+
log_field :request_user_agent, String
|
44
|
+
|
45
|
+
#
|
46
|
+
# response
|
47
|
+
#
|
48
|
+
|
49
|
+
log_field :response_length, Integer
|
50
|
+
log_field :response_status, Integer
|
51
|
+
|
52
|
+
#
|
53
|
+
# timing
|
54
|
+
#
|
55
|
+
|
56
|
+
log_field :timing_total_elapsed, Float
|
57
|
+
end
|
58
|
+
|
59
|
+
def initialize(app, emitter:)
|
60
|
+
@app = app
|
61
|
+
@emitter = emitter
|
62
|
+
end
|
63
|
+
|
64
|
+
def call(env)
|
65
|
+
begin
|
66
|
+
start = Time.now
|
67
|
+
status, headers, response = @app.call(env)
|
68
|
+
ensure
|
69
|
+
begin
|
70
|
+
line = LogLine.new
|
71
|
+
|
72
|
+
#
|
73
|
+
# error
|
74
|
+
#
|
75
|
+
|
76
|
+
if error = env["pliny.error"]
|
77
|
+
line.error_class = error.class.name
|
78
|
+
line.error_message = error.message
|
79
|
+
if error.is_a?(Pliny::Errors::Error)
|
80
|
+
line.error_id = error.id.to_s
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
#
|
85
|
+
# request
|
86
|
+
#
|
87
|
+
|
88
|
+
request = Rack::Request.new(env)
|
89
|
+
line.request_id = env["REQUEST_ID"]
|
90
|
+
line.request_ip = request.ip
|
91
|
+
line.request_method = request.request_method
|
92
|
+
line.request_path = request.path_info
|
93
|
+
line.request_user_agent = request.user_agent
|
94
|
+
if route = env["sinatra.route"]
|
95
|
+
line.request_route_signature = route.split(" ").last
|
96
|
+
end
|
97
|
+
|
98
|
+
#
|
99
|
+
# response
|
100
|
+
#
|
101
|
+
|
102
|
+
if length = headers["Content-Length"]
|
103
|
+
line.response_length = length.to_i
|
104
|
+
end
|
105
|
+
line.response_status = status
|
106
|
+
|
107
|
+
#
|
108
|
+
# timing
|
109
|
+
#
|
110
|
+
|
111
|
+
line.timing_total_elapsed = (Time.now - start).to_f
|
112
|
+
|
113
|
+
@emitter.call(line.to_h)
|
114
|
+
rescue => e
|
115
|
+
# We hope that a canonical log line never fails, but in case it
|
116
|
+
# does, do not fail the request because it did.
|
117
|
+
Pliny.log(message: "Failed to emit canonical log line")
|
118
|
+
Pliny::ErrorReporters.notify(e, rack_env: env)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
[status, headers, response]
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
@@ -9,12 +9,20 @@ module Pliny::Middleware
|
|
9
9
|
def call(env)
|
10
10
|
@app.call(env)
|
11
11
|
rescue Pliny::Errors::Error => e
|
12
|
+
set_error_in_env(env, e)
|
12
13
|
Pliny::Errors::Error.render(e)
|
13
14
|
rescue => e
|
15
|
+
set_error_in_env(env, e)
|
14
16
|
raise if @raise
|
15
17
|
|
16
18
|
Pliny::ErrorReporters.notify(e, rack_env: env)
|
17
19
|
Pliny::Errors::Error.render(Pliny::Errors::InternalServerError.new(@message))
|
18
20
|
end
|
21
|
+
|
22
|
+
# Sets the error in a predefined env key for use by the upstream
|
23
|
+
# CanonicalLogLine middleware.
|
24
|
+
def set_error_in_env(env, e)
|
25
|
+
env["pliny.error"] = e
|
26
|
+
end
|
19
27
|
end
|
20
28
|
end
|
data/lib/pliny/version.rb
CHANGED
data/lib/template/Gemfile
CHANGED
data/lib/template/lib/routes.rb
CHANGED
@@ -4,7 +4,10 @@ Routes = Rack::Builder.new do
|
|
4
4
|
use Pliny::Middleware::RequestID
|
5
5
|
use Pliny::Middleware::RequestStore::Seed, store: Pliny::RequestStore
|
6
6
|
use Pliny::Middleware::Metrics
|
7
|
-
use Pliny::Middleware::
|
7
|
+
use Pliny::Middleware::CanonicalLogLine,
|
8
|
+
emitter: -> (data) {
|
9
|
+
Pliny.log_without_context({ canonical_log_line: true }.merge(data))
|
10
|
+
}
|
8
11
|
use Pliny::Middleware::RescueErrors, raise: Config.raise_errors?
|
9
12
|
if Config.timeout.positive?
|
10
13
|
use Rack::Timeout,
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Pliny::CanonicalLogLineHelpers do
|
4
|
+
class TestCanonicalLogLine
|
5
|
+
include Pliny::CanonicalLogLineHelpers
|
6
|
+
|
7
|
+
log_field :field_float, Float
|
8
|
+
log_field :field_integer, Integer
|
9
|
+
log_field :field_string, String
|
10
|
+
end
|
11
|
+
|
12
|
+
it "allows a field to be set" do
|
13
|
+
line = TestCanonicalLogLine.new
|
14
|
+
line.field_string = "foo"
|
15
|
+
end
|
16
|
+
|
17
|
+
it "allows nils to be set" do
|
18
|
+
line = TestCanonicalLogLine.new
|
19
|
+
line.field_string = nil
|
20
|
+
end
|
21
|
+
|
22
|
+
it "rejects values that are of the wrong type" do
|
23
|
+
line = TestCanonicalLogLine.new
|
24
|
+
e = assert_raises ArgumentError do
|
25
|
+
line.field_string = true
|
26
|
+
end
|
27
|
+
assert_equal "Expected field_string to be type String (was TrueClass)",
|
28
|
+
e.message
|
29
|
+
end
|
30
|
+
|
31
|
+
it "produces a hash with #to_h" do
|
32
|
+
line = TestCanonicalLogLine.new
|
33
|
+
line.field_float = 3.14
|
34
|
+
line.field_integer = 42
|
35
|
+
line.field_string = "foo"
|
36
|
+
assert_equal({ field_float: 3.14, field_integer: 42, field_string: "foo" },
|
37
|
+
line.to_h)
|
38
|
+
end
|
39
|
+
end
|
data/spec/log_spec.rb
CHANGED
@@ -38,6 +38,12 @@ describe Pliny::Log do
|
|
38
38
|
Pliny.log(foo: "bar")
|
39
39
|
end
|
40
40
|
|
41
|
+
it "logs without context" do
|
42
|
+
Pliny.default_context = { app: "pliny" }
|
43
|
+
expect(@io).to receive(:print).with("foo=bar\n")
|
44
|
+
Pliny.log_without_context(foo: "bar")
|
45
|
+
end
|
46
|
+
|
41
47
|
it "merges context from RequestStore" do
|
42
48
|
Pliny::RequestStore.store[:log_context] = { app: "pliny" }
|
43
49
|
expect(@io).to receive(:print).with("app=pliny foo=bar\n")
|
@@ -0,0 +1,88 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Pliny::Middleware::CanonicalLogLine do
|
4
|
+
def app
|
5
|
+
Rack::Builder.new do
|
6
|
+
run Sinatra.new {
|
7
|
+
use Pliny::Middleware::RequestID
|
8
|
+
|
9
|
+
use Pliny::Middleware::CanonicalLogLine,
|
10
|
+
emitter: -> (data) { Pliny.log_without_context(data) }
|
11
|
+
|
12
|
+
use Pliny::Middleware::RescueErrors, raise: false
|
13
|
+
|
14
|
+
get "/apps/:id" do
|
15
|
+
status 201
|
16
|
+
"hi"
|
17
|
+
end
|
18
|
+
|
19
|
+
get "/generic-error" do
|
20
|
+
raise ArgumentError, "argument error"
|
21
|
+
end
|
22
|
+
|
23
|
+
get "/pliny-error" do
|
24
|
+
raise Pliny::Errors::NotFound
|
25
|
+
end
|
26
|
+
}
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
it "emits on a successful request" do
|
31
|
+
data = {}
|
32
|
+
expect(Pliny).to receive(:log_without_context) { |d| data = d }
|
33
|
+
|
34
|
+
header "User-Agent", "rack-test"
|
35
|
+
get "/apps/123"
|
36
|
+
|
37
|
+
assert_match Pliny::Middleware::RequestID::UUID_PATTERN,
|
38
|
+
data[:request_id]
|
39
|
+
assert_equal "127.0.0.1", data[:request_ip]
|
40
|
+
assert_equal "GET", data[:request_method]
|
41
|
+
assert_equal "/apps/123", data[:request_path]
|
42
|
+
assert_equal "/apps/:id", data[:request_route_signature]
|
43
|
+
assert_equal "rack-test", data[:request_user_agent]
|
44
|
+
|
45
|
+
assert_equal 2, data[:response_length]
|
46
|
+
assert_equal 201, data[:response_status]
|
47
|
+
end
|
48
|
+
|
49
|
+
it "never fails a request on failure" do
|
50
|
+
expect(Pliny).to receive(:log).with(
|
51
|
+
message: "Failed to emit canonical log line")
|
52
|
+
expect(Pliny).to receive(:log_without_context) { |d| raise "bang!" }
|
53
|
+
|
54
|
+
get "/apps/123"
|
55
|
+
assert_equal 201, last_response.status
|
56
|
+
end
|
57
|
+
|
58
|
+
it "emits on generic error" do
|
59
|
+
data = {}
|
60
|
+
expect(Pliny).to receive(:log_without_context) { |d| data = d }
|
61
|
+
get "/generic-error"
|
62
|
+
|
63
|
+
assert_equal "ArgumentError", data[:error_class]
|
64
|
+
assert_equal "argument error", data[:error_message]
|
65
|
+
|
66
|
+
assert_equal "GET", data[:request_method]
|
67
|
+
assert_equal "/generic-error", data[:request_path]
|
68
|
+
assert_equal "/generic-error", data[:request_route_signature]
|
69
|
+
|
70
|
+
assert_equal 500, data[:response_status]
|
71
|
+
end
|
72
|
+
|
73
|
+
it "emits on Pliny error" do
|
74
|
+
data = {}
|
75
|
+
expect(Pliny).to receive(:log_without_context) { |d| data = d }
|
76
|
+
get "/pliny-error"
|
77
|
+
|
78
|
+
assert_equal "Pliny::Errors::NotFound", data[:error_class]
|
79
|
+
assert_equal "Not found.", data[:error_message]
|
80
|
+
assert_equal "not_found", data[:error_id]
|
81
|
+
|
82
|
+
assert_equal "GET", data[:request_method]
|
83
|
+
assert_equal "/pliny-error", data[:request_path]
|
84
|
+
assert_equal "/pliny-error", data[:request_route_signature]
|
85
|
+
|
86
|
+
assert_equal 404, data[:response_status]
|
87
|
+
end
|
88
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: pliny
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.24.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Brandur Leach
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2017-
|
12
|
+
date: 2017-02-16 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: activesupport
|
@@ -17,20 +17,20 @@ dependencies:
|
|
17
17
|
requirements:
|
18
18
|
- - "~>"
|
19
19
|
- !ruby/object:Gem::Version
|
20
|
-
version: '
|
20
|
+
version: '5.0'
|
21
21
|
- - ">="
|
22
22
|
- !ruby/object:Gem::Version
|
23
|
-
version:
|
23
|
+
version: 5.0.1
|
24
24
|
type: :runtime
|
25
25
|
prerelease: false
|
26
26
|
version_requirements: !ruby/object:Gem::Requirement
|
27
27
|
requirements:
|
28
28
|
- - "~>"
|
29
29
|
- !ruby/object:Gem::Version
|
30
|
-
version: '
|
30
|
+
version: '5.0'
|
31
31
|
- - ">="
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version:
|
33
|
+
version: 5.0.1
|
34
34
|
- !ruby/object:Gem::Dependency
|
35
35
|
name: multi_json
|
36
36
|
requirement: !ruby/object:Gem::Requirement
|
@@ -376,6 +376,7 @@ files:
|
|
376
376
|
- bin/pliny-new
|
377
377
|
- bin/pliny-update
|
378
378
|
- lib/pliny.rb
|
379
|
+
- lib/pliny/canonical_log_line_helpers.rb
|
379
380
|
- lib/pliny/commands/creator.rb
|
380
381
|
- lib/pliny/commands/generator.rb
|
381
382
|
- lib/pliny/commands/generator/base.rb
|
@@ -397,6 +398,7 @@ files:
|
|
397
398
|
- lib/pliny/log.rb
|
398
399
|
- lib/pliny/metrics.rb
|
399
400
|
- lib/pliny/metrics/backends/logger.rb
|
401
|
+
- lib/pliny/middleware/canonical_log_line.rb
|
400
402
|
- lib/pliny/middleware/cors.rb
|
401
403
|
- lib/pliny/middleware/instruments.rb
|
402
404
|
- lib/pliny/middleware/metrics.rb
|
@@ -469,6 +471,7 @@ files:
|
|
469
471
|
- lib/template/spec/spec_support/auto_define_rack_app.rb
|
470
472
|
- lib/template/spec/spec_support/coverage.rb
|
471
473
|
- lib/template/spec/spec_support/log.rb
|
474
|
+
- spec/canonical_log_line_helpers_spec.rb
|
472
475
|
- spec/commands/creator_spec.rb
|
473
476
|
- spec/commands/generator/base_spec.rb
|
474
477
|
- spec/commands/generator/endpoint_spec.rb
|
@@ -490,6 +493,7 @@ files:
|
|
490
493
|
- spec/log_spec.rb
|
491
494
|
- spec/metrics/backends/logger_spec.rb
|
492
495
|
- spec/metrics_spec.rb
|
496
|
+
- spec/middleware/canonical_log_line_spec.rb
|
493
497
|
- spec/middleware/cors_spec.rb
|
494
498
|
- spec/middleware/instruments_spec.rb
|
495
499
|
- spec/middleware/metrics_spec.rb
|
@@ -524,7 +528,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
524
528
|
version: '0'
|
525
529
|
requirements: []
|
526
530
|
rubyforge_project:
|
527
|
-
rubygems_version: 2.5.
|
531
|
+
rubygems_version: 2.5.1
|
528
532
|
signing_key:
|
529
533
|
specification_version: 4
|
530
534
|
summary: Basic tooling to support API apps in Sinatra
|