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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 683200fbb66f5d6b6961b584480ce759a6ed08d8
4
- data.tar.gz: 49cc9f605818c73a711c442bc3a5315dd3c75ea4
3
+ metadata.gz: 6b59272bbe834eaf737b9ffe3e4769e54f8897cf
4
+ data.tar.gz: 13bfa08975ac6f65c74a686e54cf22608bbab639
5
5
  SHA512:
6
- metadata.gz: 117170ba5de6e6bacc2c6d02c15dae9de1425fdb8be491bac7d60cf123161ce0a15fb77801b0eb83ed14067349adc59c8a4a6e84b8f5bc75e50de68870d55a7f
7
- data.tar.gz: 8e94bbf374d67232644e892ede8c2d9c919e58176172158b48be927a106d0502410c4bde61b3a1957327bffdda37796052148e5b36ce8fe927e405f4cefe701a
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
@@ -4,6 +4,10 @@ module Pliny
4
4
  log_to_stream(stdout || $stdout, merge_log_contexts(data), &block)
5
5
  end
6
6
 
7
+ def log_without_context(data, &block)
8
+ log_to_stream(stdout || $stdout, data, &block)
9
+ end
10
+
7
11
  def log_exception(e, data = {})
8
12
  exception_id = e.object_id
9
13
 
@@ -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
@@ -1,3 +1,3 @@
1
1
  module Pliny
2
- VERSION = "0.23.0"
2
+ VERSION = "0.24.0"
3
3
  end
data/lib/template/Gemfile CHANGED
@@ -4,7 +4,7 @@ ruby "2.3.3"
4
4
  gem "multi_json"
5
5
  gem "oj"
6
6
  gem "pg"
7
- gem "pliny", "~> 0.23"
7
+ gem "pliny", "~> 0.24"
8
8
  gem "pry"
9
9
  gem "puma", "~> 3"
10
10
  gem "rack-ssl"
@@ -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::Instruments
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.23.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-01-10 00:00:00.000000000 Z
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: '4.1'
20
+ version: '5.0'
21
21
  - - ">="
22
22
  - !ruby/object:Gem::Version
23
- version: 4.1.0
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: '4.1'
30
+ version: '5.0'
31
31
  - - ">="
32
32
  - !ruby/object:Gem::Version
33
- version: 4.1.0
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.2
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