loginator 0.0.7 → 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
[![Code Climate](https://codeclimate.com/github/gray-industries/loginator/badges/gpa.svg)](https://codeclimate.com/github/gray-industries/loginator)
|
4
4
|
[![Build Status](https://travis-ci.org/gray-industries/loginator.svg)](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
|