loginator 0.0.7 → 0.1.1
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/Gemfile +6 -0
- data/LICENSE.md +19 -0
- data/README.md +55 -15
- data/lib/loginator/middleware/Gemfile.middleware +1 -0
- data/lib/loginator/middleware/sinatra.rb +41 -0
- data/lib/loginator/transaction.rb +127 -11
- data/lib/loginator/version.rb +1 -1
- data/lib/loginator.rb +0 -2
- data/loginator.gemspec +1 -4
- data/spec/integration/lib/loginator/middleware/sinatra_spec.rb +60 -0
- data/spec/integration/lib/loginator/transaction_spec.rb +126 -10
- metadata +9 -70
- data/lib/loginator/jsonable_struct.rb +0 -45
- data/lib/loginator/request.rb +0 -25
- data/lib/loginator/response.rb +0 -27
- data/spec/integration/lib/loginator/request_spec.rb +0 -13
- data/spec/integration/lib/loginator/response_spec.rb +0 -13
- data/spec/support/shared/lib/loginator/jsonable_struct_spec.rb +0 -13
- data/spec/support/shared/lib/loginator/struct_with_defaults_spec.rb +0 -39
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 428aba5714b1915bcfea3a857fc67f89a155e8cc
|
4
|
+
data.tar.gz: cfb6fa8598f8a0d38a5aa82e3fd4f6c1d5910b89
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9762d9e7697e7a2b5a30cd9a5f6927f5a385a05363fe42bcecce74da35836a7b9e9d5638facff39d9c105e5553e6ca574904b7f724b1b4e14fcd1656f3bf1d0f
|
7
|
+
data.tar.gz: eac5555318d79d8e76c5aad08d3c9ca8107d3e2ddfdc4609572165cce517b94795b4a796067e25955fe8aeaaefbc0760061b9ebba59dff6900ee56b0e3c93b91
|
data/Gemfile
CHANGED
@@ -1,3 +1,9 @@
|
|
1
1
|
source 'https://rubygems.org'
|
2
2
|
|
3
3
|
gemspec
|
4
|
+
|
5
|
+
# rubocop:disable all
|
6
|
+
# This is the jankiest jank in all of janktown. I want a better way to
|
7
|
+
# manage dependencies for middleware development.
|
8
|
+
eval(File.read('lib/loginator/middleware/Gemfile.middleware'), binding)
|
9
|
+
# rubocop:enable all
|
data/LICENSE.md
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
Copyright (c) 2015 Greg Poirier
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
5
|
+
in the Software without restriction, including without limitation the rights
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
8
|
+
furnished to do so, subject to the following conditions:
|
9
|
+
|
10
|
+
The above copyright notice and this permission notice shall be included in all
|
11
|
+
copies or substantial portions of the Software.
|
12
|
+
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
19
|
+
SOFTWARE.
|
data/README.md
CHANGED
@@ -3,8 +3,21 @@
|
|
3
3
|
[](https://codeclimate.com/github/gray-industries/loginator)
|
4
4
|
[](https://travis-ci.org/gray-industries/loginator)
|
5
5
|
|
6
|
-
Loginator is a gem for standardizing
|
7
|
-
|
6
|
+
Loginator is a gem for standardizing different types of log formats. It particularly focuses on standardized
|
7
|
+
logging for service interactions in a SOA / distributed application.
|
8
|
+
|
9
|
+
I am tired of having useless logs at work, and I wanted to standardize those logs. Jordan Sissel puts it
|
10
|
+
much better than me:
|
11
|
+
|
12
|
+
> I want to be able to log in a structured way and have the log output know how that should be formatted.
|
13
|
+
> Maybe for humans, maybe for computers (as JSON), maybe as some other format. Maybe you want to log to a
|
14
|
+
> csv file because that's easy to load into Excel for analysis, but you don't want to change all your
|
15
|
+
> applications log methods?
|
16
|
+
|
17
|
+
I found this in the README.md for ruby-cabin (jordansissel/ruby-cabin). When I read this, I was pretty
|
18
|
+
floored. "Someone else gets it." This is exactly what I'm trying to accomplish with Loginator, but
|
19
|
+
I want to take the idea of Cabin in a slightly different direction. It's intended for use in my Gray
|
20
|
+
Industries projects, but if someone else finds it useful, that would be amazing.
|
8
21
|
|
9
22
|
## Installation
|
10
23
|
|
@@ -27,19 +40,46 @@ Or install it yourself as:
|
|
27
40
|
Remote APIs (be they HTTP or otherwise) follow a pattern fairly similar to that
|
28
41
|
of a common HTTP API.
|
29
42
|
|
30
|
-
###
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
+
### Transactions
|
44
|
+
|
45
|
+
Transactions include the following fields:
|
46
|
+
- uuid (string)
|
47
|
+
- timestamp (serialized as a float, otherwise Time)
|
48
|
+
- duration (float)
|
49
|
+
- path (string)
|
50
|
+
- status (integer)
|
51
|
+
- request (string)
|
52
|
+
- response (string)
|
53
|
+
- params (hash)
|
54
|
+
|
55
|
+
Custom transactions can be made by extending Transaction. These custom transactions
|
56
|
+
could include additional fields, change field types, etc. In general, however, API
|
57
|
+
transactions have all or most of these characteristics regardless of protocol.
|
58
|
+
|
59
|
+
To use a transaction, wrap your API response generation in a Transaction#begin block
|
60
|
+
like so (as seen in the Sinatra middleware):
|
61
|
+
|
62
|
+
```
|
63
|
+
Loginator::Transaction.new.begin do |txn|
|
64
|
+
txn.path = env['PATH_INFO']
|
65
|
+
txn.request = req
|
66
|
+
status, headers, body = @app.call(env)
|
67
|
+
txn.status = status
|
68
|
+
txn.response = body
|
69
|
+
[status, headers, body]
|
70
|
+
end
|
71
|
+
```
|
72
|
+
|
73
|
+
The begin method will return the last line much like a function, allowing you
|
74
|
+
to seemlessly integrate transaction logging into your middleware.
|
75
|
+
|
76
|
+
## Middleware
|
77
|
+
|
78
|
+
This Gem includes middleware. I am not adding explicit dependencies on the frameworks I'm targeting.
|
79
|
+
That being said, I do want to document the version of those frameworks and have put a separate Gemfile
|
80
|
+
in `lib/loginator/middleware` that includes the appropriate development dependencies required. I am
|
81
|
+
also adding that Gemfile to the gem's root Gemfile to make testing and contributing easier. I think
|
82
|
+
it is necessary to draw this distinction, because using Loginator does not explicitly require those gems.
|
43
83
|
|
44
84
|
## Contributing
|
45
85
|
|
@@ -0,0 +1 @@
|
|
1
|
+
gem 'sinatra', '~> 1.4.5'
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'securerandom'
|
2
|
+
|
3
|
+
module Loginator
|
4
|
+
module Middleware
|
5
|
+
# Middleware for logging transactions with Sinatra.
|
6
|
+
class Sinatra
|
7
|
+
attr_reader :app, :logger, :transaction
|
8
|
+
|
9
|
+
# @param app [Rack::App] #{Rack::App} being passed through middleware chain
|
10
|
+
# @param logger [IO] #{IO} object where log messages will be sent via puts()
|
11
|
+
def initialize(app, logger)
|
12
|
+
@app = app
|
13
|
+
@logger = logger
|
14
|
+
end
|
15
|
+
|
16
|
+
def call(env)
|
17
|
+
uuid = env['X-REQUEST-ID'] ||= SecureRandom.uuid
|
18
|
+
req = Rack::Request.new(env)
|
19
|
+
@transaction = Loginator::Transaction.new(uuid: uuid)
|
20
|
+
transaction.begin do |txn|
|
21
|
+
txn.path = env['PATH_INFO']
|
22
|
+
txn.request = read_body(req)
|
23
|
+
status, headers, body = @app.call(env)
|
24
|
+
txn.status = status
|
25
|
+
txn.response = body
|
26
|
+
[status, headers, body]
|
27
|
+
end
|
28
|
+
ensure
|
29
|
+
logger.puts(transaction.to_json)
|
30
|
+
end
|
31
|
+
|
32
|
+
protected
|
33
|
+
|
34
|
+
def read_body(req)
|
35
|
+
req.body.read
|
36
|
+
ensure
|
37
|
+
req.body.rewind
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -1,21 +1,137 @@
|
|
1
1
|
require 'securerandom'
|
2
2
|
|
3
3
|
module Loginator
|
4
|
-
#
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
4
|
+
# A Loginator::Transaction is meant to encapsulate a single
|
5
|
+
# request/response cycle.
|
6
|
+
class Transaction
|
7
|
+
class << self
|
8
|
+
# @param [String] json JSON body of the given transaction
|
9
|
+
# @return [Transaction]
|
10
|
+
def from_json(json)
|
11
|
+
obj = MultiJson.load(json, symbolize_keys: true)
|
12
|
+
Transaction.new(filter_hash(obj))
|
13
|
+
end
|
14
|
+
|
15
|
+
protected
|
16
|
+
|
17
|
+
# This is the inverse functions for the instance-level
|
18
|
+
# filter_hash.
|
19
|
+
def filter_hash(hsh)
|
20
|
+
[[:timestamp, ->(t) { Time.at(t) }]].map do |k, f|
|
21
|
+
hsh[k] = f.call(hsh[k])
|
22
|
+
end
|
23
|
+
hsh
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# The following may look funny to people, but please know that it's simply an attempt to
|
28
|
+
# convey intent. I want Transactions to be thought of as immutable objects. I'd like to
|
29
|
+
# take them in that direction eventually, but doing so in a way that is comprehensible
|
30
|
+
# is difficult. After rewriting the interface to Transaction several times, I settled here.
|
31
|
+
attr_accessor :path, :request, :params, :response, :status
|
32
|
+
|
33
|
+
attr_reader :uuid, :timestamp, :duration
|
34
|
+
|
35
|
+
protected
|
36
|
+
|
37
|
+
attr_writer :uuid, :timestamp, :duration
|
38
|
+
|
39
|
+
public
|
40
|
+
|
41
|
+
# Create a new Loginator::Transaction
|
42
|
+
# @param [Hash] opts The options to create the Loginator::Transaction
|
43
|
+
# @option opts [String] :uuid UUID (SecureRandom.uuid)
|
44
|
+
# @option opts [Time] :timestamp (Time.now) Beginning timestamp of transaction
|
45
|
+
# @option opts [Integer] :status Status returned to the requester
|
46
|
+
# @option opts [String] :path Path associated with the request
|
47
|
+
# @option opts [String] :request Body of the request
|
48
|
+
# @option opts [Hash] :params Parameters of the request
|
49
|
+
# @option opts [String] :response Body of the response
|
50
|
+
# @option opts [Float] :duration Duration of the request
|
51
|
+
def initialize(opts = {})
|
52
|
+
# TODO: UUID Generation should have a service interface
|
53
|
+
@uuid = opts.delete(:uuid) || SecureRandom.uuid
|
54
|
+
@timestamp = opts.delete(:timestamp) || Time.now
|
55
|
+
opts.each_pair do |k, v|
|
56
|
+
send("#{k}=", v)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# Marks the beginning of a Transaction. Optionally, will execute
|
61
|
+
# the given block in the context of a transaction, returning the
|
62
|
+
# last line of the block and raising any exceptions thrown in the
|
63
|
+
# block given.
|
64
|
+
#
|
65
|
+
# NOTE: we make a best effort to guarantee that `transaction.finished`
|
66
|
+
# is called via an ensure block, but that is all. We do not set the
|
67
|
+
# status or response. You must do so in your block if you wish to
|
68
|
+
# record any failures.
|
69
|
+
# @param [Block] &blk optional
|
70
|
+
# @yield [Transaction] the transaction object after it has been updated
|
71
|
+
# @return [] Returns whatever the block returns
|
72
|
+
def begin
|
73
|
+
@timestamp = Time.now
|
74
|
+
# NOTE: yield self is a bit of a smell to me, but I am okay with this
|
75
|
+
# as the block is evaluated in the context of the caller and not of
|
76
|
+
# the Transaction object.
|
77
|
+
yield self if block_given?
|
78
|
+
ensure
|
79
|
+
finished
|
80
|
+
end
|
81
|
+
|
82
|
+
# Marks the end of the transaction.
|
83
|
+
#
|
84
|
+
# NOTE: Once set, duration should be considered immutable. I.e. you
|
85
|
+
# cannot affect the duration of this Transaction by calling finished
|
86
|
+
# twice.
|
87
|
+
# @return [Time] time of the end of the transaction
|
88
|
+
def finished
|
89
|
+
fin = Time.now
|
90
|
+
@duration ||= calculate_duration(fin)
|
91
|
+
fin
|
92
|
+
end
|
93
|
+
|
94
|
+
# Hashify the Transaction
|
95
|
+
# @return [Hash]
|
96
|
+
def to_h
|
97
|
+
[:uuid, :timestamp, :status, :path, :request, :params, :response, :duration].each_with_object({}) do |key, hsh|
|
98
|
+
hsh[key] = send(key)
|
99
|
+
end
|
9
100
|
end
|
10
101
|
|
11
|
-
#
|
12
|
-
|
13
|
-
|
102
|
+
# JSONify the Transaction
|
103
|
+
# @return [String]
|
104
|
+
def to_json
|
105
|
+
MultiJson.dump(filter_hash!(to_h))
|
106
|
+
end
|
107
|
+
|
108
|
+
# Two Transactions are considered equivalent if the following
|
109
|
+
# are all equal:
|
110
|
+
# UUID, Duration, Response, Path, Request, Parameters.
|
111
|
+
# This is largely used for testing serialization/deserialization.
|
112
|
+
def ==(other)
|
113
|
+
timestamp.to_f == other.timestamp.to_f &&
|
114
|
+
[:uuid, :duration, :response, :path, :request, :params].all? { |k| send(k) == other.send(k) }
|
115
|
+
end
|
116
|
+
|
117
|
+
protected
|
118
|
+
|
119
|
+
# @param [Time] t ending timestamp of the transaction
|
120
|
+
# @return [Float] duration of the transaction
|
121
|
+
def calculate_duration(t)
|
122
|
+
(t - timestamp).to_f
|
14
123
|
end
|
15
124
|
|
16
|
-
#
|
17
|
-
|
18
|
-
|
125
|
+
# Filter the transaction hash's elements based on a mapping of
|
126
|
+
# element->proc pairs. This is largely for serialization/deserialization.
|
127
|
+
# NOTE: This modifies the hash in place.
|
128
|
+
# @param [Hash] hsh the hash to be modified
|
129
|
+
# @return [Hash] the modified hash
|
130
|
+
def filter_hash!(hsh)
|
131
|
+
[[:timestamp, ->(t) { t.to_f }]].each do |k, f|
|
132
|
+
hsh[k] = f.call(hsh[k])
|
133
|
+
end
|
134
|
+
hsh
|
19
135
|
end
|
20
136
|
end
|
21
137
|
end
|
data/lib/loginator/version.rb
CHANGED
data/lib/loginator.rb
CHANGED
data/loginator.gemspec
CHANGED
@@ -9,6 +9,7 @@ Gem::Specification.new do |gem|
|
|
9
9
|
gem.description = 'Standardized logging of API requests/responses'
|
10
10
|
gem.summary = 'Loginator is a mechanism for standardizing the logging of API requests and responses.'
|
11
11
|
|
12
|
+
gem.licenses = ['MIT']
|
12
13
|
gem.files = `git ls-files`.split($OUTPUT_RECORD_SEPARATOR)
|
13
14
|
gem.executables = gem.files.grep(/^bin\//).map { |f| File.basename(f) }
|
14
15
|
gem.test_files = gem.files.grep(/^(test|spec|features)\//)
|
@@ -17,10 +18,6 @@ Gem::Specification.new do |gem|
|
|
17
18
|
gem.version = Loginator::VERSION
|
18
19
|
|
19
20
|
# dependencies...
|
20
|
-
gem.add_dependency('thor', '0.19.1')
|
21
|
-
gem.add_dependency('sysexits', '1.0.2')
|
22
|
-
gem.add_dependency('awesome_print', '~> 1.1.0')
|
23
|
-
gem.add_dependency('abstract_type', '~> 0.0.7')
|
24
21
|
gem.add_dependency('multi_json', '~> 1.10.1')
|
25
22
|
|
26
23
|
# development dependencies.
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require 'loginator/middleware/sinatra'
|
2
|
+
require 'rack'
|
3
|
+
|
4
|
+
describe Loginator::Middleware::Sinatra do
|
5
|
+
let(:app) { ->(_env) { response } }
|
6
|
+
let(:response) { [200, env, 'app'] }
|
7
|
+
let(:env) { Rack::MockRequest.env_for('http://test/path', input: body) }
|
8
|
+
let(:body) { StringIO.new('Request body') }
|
9
|
+
let(:logger) { StringIO.new }
|
10
|
+
let(:transaction) { subject.transaction }
|
11
|
+
|
12
|
+
subject { described_class.new(app, logger) }
|
13
|
+
|
14
|
+
context 'app call succeeds' do
|
15
|
+
before do
|
16
|
+
subject.call(env)
|
17
|
+
end
|
18
|
+
|
19
|
+
it 'adds the UUID to the sinatra env' do
|
20
|
+
expect(transaction.uuid).to be_a_kind_of(String)
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'rewinds the Sinatra request body' do
|
24
|
+
expect(body.eof?).to be_falsey
|
25
|
+
end
|
26
|
+
|
27
|
+
it 'sets the request path' do
|
28
|
+
expect(transaction.path).to eq('/path')
|
29
|
+
end
|
30
|
+
|
31
|
+
it 'sets the request body' do
|
32
|
+
expect(transaction.request).to eq(body.string)
|
33
|
+
end
|
34
|
+
|
35
|
+
it 'sets the timestamp' do
|
36
|
+
expect(transaction.timestamp).to be_a_kind_of(Time)
|
37
|
+
end
|
38
|
+
|
39
|
+
it 'sets the response code' do
|
40
|
+
expect(transaction.status).to eq(200)
|
41
|
+
end
|
42
|
+
|
43
|
+
it 'sets the response body' do
|
44
|
+
expect(transaction.response).to eq('app')
|
45
|
+
end
|
46
|
+
|
47
|
+
it 'logs the transaction' do
|
48
|
+
expect(logger.string.chomp).to eq(transaction.to_json)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
context 'app call fails' do
|
53
|
+
let(:app) { ->(_env) { fail } }
|
54
|
+
before { expect { subject.call(env) }.to raise_error }
|
55
|
+
|
56
|
+
it 'still logs the message' do
|
57
|
+
expect(logger.string.chomp).to eq(transaction.to_json)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -1,23 +1,139 @@
|
|
1
1
|
require 'loginator/transaction'
|
2
2
|
|
3
3
|
RSpec.describe Loginator::Transaction do
|
4
|
-
|
4
|
+
describe '#new' do
|
5
|
+
before do
|
6
|
+
subject.finished
|
7
|
+
end
|
5
8
|
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
+
context 'using defaults' do
|
10
|
+
it 'creates a valid object' do
|
11
|
+
expect(subject).to be_a_kind_of(described_class)
|
12
|
+
end
|
13
|
+
|
14
|
+
it 'sets default request id' do
|
15
|
+
expect(subject.uuid).to be_a_kind_of(String)
|
16
|
+
end
|
17
|
+
|
18
|
+
it 'sets default timestamp' do
|
19
|
+
expect(subject.timestamp).to be_a_kind_of(Time)
|
20
|
+
expect(subject.timestamp).to be <= Time.now
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
context 'overriding defaults' do
|
25
|
+
let(:uuid) { 'uuid' }
|
26
|
+
let(:timestamp) { Time.at(42) }
|
27
|
+
let(:path) { '/' }
|
28
|
+
|
29
|
+
subject { described_class.new(uuid: uuid, timestamp: timestamp, duration: 0, path: path) }
|
30
|
+
|
31
|
+
[:uuid, :timestamp, :path].each do |field|
|
32
|
+
it 'overrides defaults' do
|
33
|
+
expect(subject.send(field)).to eq(send(field))
|
34
|
+
end
|
35
|
+
end
|
9
36
|
|
10
|
-
|
11
|
-
|
37
|
+
describe '.from_json' do
|
38
|
+
it 'faithfully deserializes' do
|
39
|
+
expect(described_class.from_json(subject.to_json)).to eq(subject)
|
40
|
+
end
|
12
41
|
end
|
13
42
|
end
|
43
|
+
end
|
44
|
+
|
45
|
+
describe '#finished' do
|
46
|
+
before do
|
47
|
+
subject.finished
|
48
|
+
end
|
49
|
+
|
50
|
+
it 'sets a positive float for the duration' do
|
51
|
+
expect(subject.duration).to be_a_kind_of(Float)
|
52
|
+
expect(subject.duration).to be > 0.0
|
53
|
+
end
|
54
|
+
end
|
14
55
|
|
15
|
-
|
16
|
-
|
56
|
+
describe '#begin' do
|
57
|
+
shared_examples_for 'transaction#begin' do
|
58
|
+
it 'marks the beginning of the transaction' do
|
59
|
+
expect(subject.timestamp).to be_a_kind_of(Time)
|
60
|
+
end
|
61
|
+
|
62
|
+
it 'executes the block' do
|
63
|
+
expect(@in_block.to_f).to be > subject.timestamp.to_f
|
64
|
+
end
|
17
65
|
|
18
|
-
it '
|
19
|
-
expect(
|
66
|
+
it 'marks the end of the transaction after the block is finished' do
|
67
|
+
expect((subject.timestamp + subject.duration).to_f).to be > @in_block.to_f
|
20
68
|
end
|
21
69
|
end
|
70
|
+
|
71
|
+
context 'when the block executes normally' do
|
72
|
+
before do
|
73
|
+
subject.begin do
|
74
|
+
sleep 0.01
|
75
|
+
@in_block = Time.now
|
76
|
+
end
|
77
|
+
|
78
|
+
it_behaves_like 'transaction#begin'
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
context 'when the block passed raises an exception' do
|
83
|
+
before do
|
84
|
+
expect do
|
85
|
+
subject.begin do
|
86
|
+
sleep 0.01
|
87
|
+
@in_block = Time.now
|
88
|
+
fail
|
89
|
+
end
|
90
|
+
end.to raise_error
|
91
|
+
end
|
92
|
+
|
93
|
+
it_behaves_like 'transaction#begin'
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
describe '#to_json' do
|
98
|
+
let(:json) { MultiJson.dump(hash) }
|
99
|
+
|
100
|
+
let(:hash) do
|
101
|
+
{
|
102
|
+
'uuid' => '1',
|
103
|
+
'timestamp' => 0.0,
|
104
|
+
'duration' => 1.0,
|
105
|
+
'path' => '/',
|
106
|
+
'status' => 0,
|
107
|
+
'request' => '',
|
108
|
+
'response' => '',
|
109
|
+
'params' => {}
|
110
|
+
}
|
111
|
+
end
|
112
|
+
|
113
|
+
subject { described_class.from_json(json) }
|
114
|
+
|
115
|
+
before do
|
116
|
+
subject.finished
|
117
|
+
end
|
118
|
+
|
119
|
+
it 'faithfully serializes' do
|
120
|
+
%w(uuid duration path status request response params).each do |k|
|
121
|
+
expect(subject.send(k)).to eq(hash[k])
|
122
|
+
end
|
123
|
+
|
124
|
+
expect(subject.timestamp.to_f).to eq(hash['timestamp'])
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
describe '.from_json' do
|
129
|
+
subject { described_class.new(duration: 1.0, timestamp: Time.now) }
|
130
|
+
|
131
|
+
before do
|
132
|
+
subject.finished
|
133
|
+
end
|
134
|
+
|
135
|
+
it 'faithfully deserializes' do
|
136
|
+
expect(described_class.from_json(subject.to_json)).to eq(subject)
|
137
|
+
end
|
22
138
|
end
|
23
139
|
end
|
metadata
CHANGED
@@ -1,71 +1,15 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: loginator
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.1.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Greg Poirier
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-
|
11
|
+
date: 2015-03-09 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
|
-
- !ruby/object:Gem::Dependency
|
14
|
-
name: thor
|
15
|
-
requirement: !ruby/object:Gem::Requirement
|
16
|
-
requirements:
|
17
|
-
- - '='
|
18
|
-
- !ruby/object:Gem::Version
|
19
|
-
version: 0.19.1
|
20
|
-
type: :runtime
|
21
|
-
prerelease: false
|
22
|
-
version_requirements: !ruby/object:Gem::Requirement
|
23
|
-
requirements:
|
24
|
-
- - '='
|
25
|
-
- !ruby/object:Gem::Version
|
26
|
-
version: 0.19.1
|
27
|
-
- !ruby/object:Gem::Dependency
|
28
|
-
name: sysexits
|
29
|
-
requirement: !ruby/object:Gem::Requirement
|
30
|
-
requirements:
|
31
|
-
- - '='
|
32
|
-
- !ruby/object:Gem::Version
|
33
|
-
version: 1.0.2
|
34
|
-
type: :runtime
|
35
|
-
prerelease: false
|
36
|
-
version_requirements: !ruby/object:Gem::Requirement
|
37
|
-
requirements:
|
38
|
-
- - '='
|
39
|
-
- !ruby/object:Gem::Version
|
40
|
-
version: 1.0.2
|
41
|
-
- !ruby/object:Gem::Dependency
|
42
|
-
name: awesome_print
|
43
|
-
requirement: !ruby/object:Gem::Requirement
|
44
|
-
requirements:
|
45
|
-
- - "~>"
|
46
|
-
- !ruby/object:Gem::Version
|
47
|
-
version: 1.1.0
|
48
|
-
type: :runtime
|
49
|
-
prerelease: false
|
50
|
-
version_requirements: !ruby/object:Gem::Requirement
|
51
|
-
requirements:
|
52
|
-
- - "~>"
|
53
|
-
- !ruby/object:Gem::Version
|
54
|
-
version: 1.1.0
|
55
|
-
- !ruby/object:Gem::Dependency
|
56
|
-
name: abstract_type
|
57
|
-
requirement: !ruby/object:Gem::Requirement
|
58
|
-
requirements:
|
59
|
-
- - "~>"
|
60
|
-
- !ruby/object:Gem::Version
|
61
|
-
version: 0.0.7
|
62
|
-
type: :runtime
|
63
|
-
prerelease: false
|
64
|
-
version_requirements: !ruby/object:Gem::Requirement
|
65
|
-
requirements:
|
66
|
-
- - "~>"
|
67
|
-
- !ruby/object:Gem::Version
|
68
|
-
version: 0.0.7
|
69
13
|
- !ruby/object:Gem::Dependency
|
70
14
|
name: multi_json
|
71
15
|
requirement: !ruby/object:Gem::Requirement
|
@@ -262,14 +206,14 @@ files:
|
|
262
206
|
- ".travis.yml"
|
263
207
|
- Gemfile
|
264
208
|
- Guardfile
|
209
|
+
- LICENSE.md
|
265
210
|
- README.md
|
266
211
|
- Rakefile
|
267
212
|
- examples/rack/logging.rb
|
268
213
|
- examples/rack/uuid.rb
|
269
214
|
- lib/loginator.rb
|
270
|
-
- lib/loginator/
|
271
|
-
- lib/loginator/
|
272
|
-
- lib/loginator/response.rb
|
215
|
+
- lib/loginator/middleware/Gemfile.middleware
|
216
|
+
- lib/loginator/middleware/sinatra.rb
|
273
217
|
- lib/loginator/transaction.rb
|
274
218
|
- lib/loginator/version.rb
|
275
219
|
- loginator.gemspec
|
@@ -277,17 +221,15 @@ files:
|
|
277
221
|
- spec/fixtures.rb
|
278
222
|
- spec/fixtures/.gitignore
|
279
223
|
- spec/integration/lib/loginator/.gitignore
|
280
|
-
- spec/integration/lib/loginator/
|
281
|
-
- spec/integration/lib/loginator/response_spec.rb
|
224
|
+
- spec/integration/lib/loginator/middleware/sinatra_spec.rb
|
282
225
|
- spec/integration/lib/loginator/transaction_spec.rb
|
283
226
|
- spec/resources.rb
|
284
227
|
- spec/resources/.gitignore
|
285
228
|
- spec/spec_helper.rb
|
286
|
-
- spec/support/shared/lib/loginator/jsonable_struct_spec.rb
|
287
|
-
- spec/support/shared/lib/loginator/struct_with_defaults_spec.rb
|
288
229
|
- spec/unit/lib/loginator/.gitignore
|
289
230
|
homepage:
|
290
|
-
licenses:
|
231
|
+
licenses:
|
232
|
+
- MIT
|
291
233
|
metadata: {}
|
292
234
|
post_install_message:
|
293
235
|
rdoc_options: []
|
@@ -315,13 +257,10 @@ test_files:
|
|
315
257
|
- spec/fixtures.rb
|
316
258
|
- spec/fixtures/.gitignore
|
317
259
|
- spec/integration/lib/loginator/.gitignore
|
318
|
-
- spec/integration/lib/loginator/
|
319
|
-
- spec/integration/lib/loginator/response_spec.rb
|
260
|
+
- spec/integration/lib/loginator/middleware/sinatra_spec.rb
|
320
261
|
- spec/integration/lib/loginator/transaction_spec.rb
|
321
262
|
- spec/resources.rb
|
322
263
|
- spec/resources/.gitignore
|
323
264
|
- spec/spec_helper.rb
|
324
|
-
- spec/support/shared/lib/loginator/jsonable_struct_spec.rb
|
325
|
-
- spec/support/shared/lib/loginator/struct_with_defaults_spec.rb
|
326
265
|
- spec/unit/lib/loginator/.gitignore
|
327
266
|
has_rdoc:
|
@@ -1,45 +0,0 @@
|
|
1
|
-
require 'multi_json'
|
2
|
-
|
3
|
-
module Loginator
|
4
|
-
# Makes a Struct easily serializable and deserializable. Adds the
|
5
|
-
# from_json class method and to_json instance method to Struct
|
6
|
-
# classes.
|
7
|
-
module JsonableStruct
|
8
|
-
def self.included(base)
|
9
|
-
base.extend ClassMethods
|
10
|
-
end
|
11
|
-
|
12
|
-
# class level mixins
|
13
|
-
module ClassMethods #:nodoc
|
14
|
-
def from_hash(hsh)
|
15
|
-
hsh_type = hsh.delete('type')
|
16
|
-
fail(ArgumentError, format('Incorrect message type: %s', hsh_type)) unless hsh_type == type
|
17
|
-
fail(ArgumentError, format('Hash must contain keys: %s', members.join(', '))) unless valid_hash?(hsh)
|
18
|
-
new(*hsh.values)
|
19
|
-
end
|
20
|
-
|
21
|
-
def from_json(json_str)
|
22
|
-
json = MultiJson.load(json_str)
|
23
|
-
from_hash(json)
|
24
|
-
end
|
25
|
-
|
26
|
-
def type
|
27
|
-
@type ||= name.split('::').last.downcase.freeze
|
28
|
-
end
|
29
|
-
|
30
|
-
private
|
31
|
-
|
32
|
-
def valid_hash?(hsh)
|
33
|
-
# Loosely validate that all necessary keys are present...
|
34
|
-
# This does mean that we could potentially create a Response from a hash
|
35
|
-
# representing an object _like_ a response.... Feature or... ? ^_^
|
36
|
-
members & hsh.keys.map(&:to_sym) == members
|
37
|
-
end
|
38
|
-
end #:rubocop:enable documentation
|
39
|
-
|
40
|
-
def to_json
|
41
|
-
MultiJson.dump(to_h.merge(type: self.class.type))
|
42
|
-
end
|
43
|
-
alias_method :to_s, :to_json
|
44
|
-
end
|
45
|
-
end
|
data/lib/loginator/request.rb
DELETED
@@ -1,25 +0,0 @@
|
|
1
|
-
require 'multi_json'
|
2
|
-
require 'loginator/jsonable_struct'
|
3
|
-
require 'loginator/transaction'
|
4
|
-
|
5
|
-
# Loginator::Request
|
6
|
-
module Loginator
|
7
|
-
# A request is a tuple of a (UUID, Timestamp, Path, Parameters). Requests parameters are defined subjectively by the
|
8
|
-
# user. For example, in the example Rack middleware ({#Rack::Loginator::Logging}), we define params as the request
|
9
|
-
# body (our HTTP APIs tend to accept JSON bodies as opposed to parameters attached to the URL). You may also wish
|
10
|
-
# to consider part of the HTTP headers as a request parameter.
|
11
|
-
#
|
12
|
-
Request = Struct.new(:request_id, :timestamp, :path, :params) do
|
13
|
-
include Loginator::JsonableStruct
|
14
|
-
include Loginator::Transaction
|
15
|
-
|
16
|
-
# Create a new Loginator::Request
|
17
|
-
# @param request_id [String] (SecureRandom.uuid) Unique identifier for the request
|
18
|
-
# @param timestamp [Float] (Time.now.utc.to_f) Time of the request
|
19
|
-
# @param path [String] (nil) Path associated with the request
|
20
|
-
# @param params [String] ({}) Parameters of the request
|
21
|
-
def initialize(request_id = uuid, timestamp = format_time, path = nil, params = {})
|
22
|
-
super
|
23
|
-
end
|
24
|
-
end
|
25
|
-
end
|
data/lib/loginator/response.rb
DELETED
@@ -1,27 +0,0 @@
|
|
1
|
-
require 'multi_json'
|
2
|
-
require 'loginator/jsonable_struct'
|
3
|
-
require 'loginator/transaction'
|
4
|
-
|
5
|
-
# Loginator::Response
|
6
|
-
module Loginator
|
7
|
-
# A {#Loginator::Response} is a response to a {#Loginator::Request}. It should include the same elements as a
|
8
|
-
# request, plus the status of the response (an indicator if the API request was successful or not) as well
|
9
|
-
# as an optional response body. Whether or not to log the response is entirely left up to implementation
|
10
|
-
# decisions and production log volume considerations. It is trivial to log response bodies in development,
|
11
|
-
# but not in production.
|
12
|
-
#
|
13
|
-
Response = Struct.new(:request_id, :timestamp, :path, :status, :body) do
|
14
|
-
include Loginator::JsonableStruct
|
15
|
-
include Loginator::Transaction
|
16
|
-
|
17
|
-
# Create a new Loginator::Response
|
18
|
-
# @param request_id [String] (SecureRandom.uuid) Unique identifier for the request
|
19
|
-
# @param timestamp [Float] (Time.now.utc.to_f) Time of the request
|
20
|
-
# @param path [String] (nil) Path associated with the request
|
21
|
-
# @param status [Integer] (0) Status returned to the requester
|
22
|
-
# @param body [String] ({}) Parameters of the request
|
23
|
-
def initialize(request_id = uuid, timestamp = format_time, path = nil, status = 0, body = '')
|
24
|
-
super
|
25
|
-
end
|
26
|
-
end
|
27
|
-
end
|
@@ -1,13 +0,0 @@
|
|
1
|
-
require 'loginator/request'
|
2
|
-
require 'support/shared/lib/loginator/jsonable_struct_spec'
|
3
|
-
require 'support/shared/lib/loginator/struct_with_defaults_spec'
|
4
|
-
|
5
|
-
RSpec.describe Loginator::Request do
|
6
|
-
describe '#new' do
|
7
|
-
it_behaves_like 'struct_with_defaults'
|
8
|
-
end
|
9
|
-
|
10
|
-
describe 'serializability' do
|
11
|
-
it_behaves_like 'jsonable_struct'
|
12
|
-
end
|
13
|
-
end
|
@@ -1,13 +0,0 @@
|
|
1
|
-
require 'loginator/response'
|
2
|
-
require 'support/shared/lib/loginator/jsonable_struct_spec'
|
3
|
-
require 'support/shared/lib/loginator/struct_with_defaults_spec'
|
4
|
-
|
5
|
-
RSpec.describe Loginator::Response do
|
6
|
-
describe '#new' do
|
7
|
-
it_behaves_like 'struct_with_defaults'
|
8
|
-
end
|
9
|
-
|
10
|
-
describe 'serializability' do
|
11
|
-
it_behaves_like 'jsonable_struct'
|
12
|
-
end
|
13
|
-
end
|
@@ -1,13 +0,0 @@
|
|
1
|
-
shared_examples_for 'jsonable_struct' do
|
2
|
-
describe '#to_json' do
|
3
|
-
it 'faithfully serializes' do
|
4
|
-
expect(subject.to_json).to eq(subject.to_h.merge(type: described_class.type).to_json)
|
5
|
-
end
|
6
|
-
end
|
7
|
-
|
8
|
-
describe '.from_json' do
|
9
|
-
it 'faithfully deserializes' do
|
10
|
-
expect(described_class.from_json(subject.to_json)).to eq(subject)
|
11
|
-
end
|
12
|
-
end
|
13
|
-
end
|
@@ -1,39 +0,0 @@
|
|
1
|
-
shared_examples_for 'struct_with_defaults' do
|
2
|
-
describe '#new' do
|
3
|
-
context 'using defaults' do
|
4
|
-
it 'creates a valid object' do
|
5
|
-
expect(subject).to be_a_kind_of(described_class)
|
6
|
-
end
|
7
|
-
|
8
|
-
it 'sets default request id' do
|
9
|
-
expect(subject.request_id).to be_a_kind_of(String)
|
10
|
-
end
|
11
|
-
|
12
|
-
it 'sets default timestamp' do
|
13
|
-
expect(subject.timestamp).to be_a_kind_of(Float)
|
14
|
-
expect(subject.timestamp).to be <= Time.now.utc.to_f
|
15
|
-
expect(subject.timestamp).to be > 0.0
|
16
|
-
end
|
17
|
-
end
|
18
|
-
|
19
|
-
context 'overriding defaults' do
|
20
|
-
let(:request_id) { 'uuid' }
|
21
|
-
let(:timestamp) { 42 }
|
22
|
-
let(:path) { '/' }
|
23
|
-
|
24
|
-
subject { described_class.new(request_id, timestamp, path) }
|
25
|
-
|
26
|
-
[:request_id, :timestamp, :path].each do |field|
|
27
|
-
it 'overrides defaults' do
|
28
|
-
expect(subject.send(field)).to eq(send(field))
|
29
|
-
end
|
30
|
-
end
|
31
|
-
|
32
|
-
describe '.from_json' do
|
33
|
-
it 'faithfully deserializes' do
|
34
|
-
expect(described_class.from_json(subject.to_json)).to eq(subject)
|
35
|
-
end
|
36
|
-
end
|
37
|
-
end
|
38
|
-
end
|
39
|
-
end
|