pliny 0.23.0 → 0.24.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|