lines 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +18 -0
- data/lib/lines.rb +343 -0
- data/lib/lines/active_record.rb +36 -0
- data/lib/lines/loader.rb +166 -0
- data/lib/lines/logger.rb +61 -0
- data/lib/lines/rack_logger.rb +38 -0
- data/lib/lines/version.rb +3 -0
- metadata +51 -0
data/README.md
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
Lines - structured logs for humans
|
2
|
+
==================================
|
3
|
+
|
4
|
+
An oppinionated logging library that implement the
|
5
|
+
[lines](https://github.com/zimbatm/lines) format.
|
6
|
+
|
7
|
+
Status: work in progress
|
8
|
+
|
9
|
+
TODO
|
10
|
+
====
|
11
|
+
|
12
|
+
Add context to the library
|
13
|
+
|
14
|
+
Check performances
|
15
|
+
|
16
|
+
Exception formatting
|
17
|
+
|
18
|
+
Fix the UniqueID algorithm
|
data/lib/lines.rb
ADDED
@@ -0,0 +1,343 @@
|
|
1
|
+
require 'date'
|
2
|
+
require 'time'
|
3
|
+
require 'forwardable'
|
4
|
+
|
5
|
+
# Lines is an opinionated structured log format and a library
|
6
|
+
# inspired by Slogger.
|
7
|
+
#
|
8
|
+
# Don't use log levels. They limit the reasoning of the developer.
|
9
|
+
# Log everything in development AND production.
|
10
|
+
# Logs should be easy to read, grep and parse.
|
11
|
+
# Logging something should never fail.
|
12
|
+
# Use syslog.
|
13
|
+
#
|
14
|
+
# Example:
|
15
|
+
#
|
16
|
+
# log(msg: "Oops !")
|
17
|
+
# #outputs:
|
18
|
+
# # at=2013-03-07T09:21:39+00:00 pid=3242 app=some-process msg="Oops !" foo={} g=[]
|
19
|
+
#
|
20
|
+
# Usage:
|
21
|
+
#
|
22
|
+
# Lines.use(Syslog, $stderr)
|
23
|
+
# Lines.log(foo: 3, msg: "This")
|
24
|
+
#
|
25
|
+
# ctx = Lines.context(encoding_id: Log.id)
|
26
|
+
# ctx.log({})
|
27
|
+
#
|
28
|
+
# Lines.context(:foo => :bar) do |l|
|
29
|
+
# l.log(:sadfasdf => 3)
|
30
|
+
# end
|
31
|
+
module Lines
|
32
|
+
# New lines in Lines
|
33
|
+
NL = "\n".freeze
|
34
|
+
|
35
|
+
@global = {}
|
36
|
+
@outputters = []
|
37
|
+
|
38
|
+
class << self
|
39
|
+
def dumper; @dumper ||= Dumper.new end
|
40
|
+
attr_reader :global
|
41
|
+
attr_reader :outputters
|
42
|
+
|
43
|
+
# Used to select what output the lines will be put on.
|
44
|
+
#
|
45
|
+
# outputs - allows any kind of IO or Syslog
|
46
|
+
#
|
47
|
+
# Usage:
|
48
|
+
#
|
49
|
+
# Lines.use(Syslog, $stderr)
|
50
|
+
def use(*outputs)
|
51
|
+
outputters.replace(outputs.map{|o| to_outputter o})
|
52
|
+
end
|
53
|
+
|
54
|
+
# The main function. Used to record objects in the logs as lines.
|
55
|
+
#
|
56
|
+
# obj - a ruby hash
|
57
|
+
# args -
|
58
|
+
def log(obj, args={})
|
59
|
+
obj = sanitize_obj(obj, args)
|
60
|
+
obj = global.merge(obj)
|
61
|
+
outputters.each{|out| out.output(dumper, obj) }
|
62
|
+
obj
|
63
|
+
end
|
64
|
+
|
65
|
+
# Add data to the logs
|
66
|
+
#
|
67
|
+
# data - a ruby hash
|
68
|
+
def context(data={})
|
69
|
+
new_context = Context.new global.merge(data)
|
70
|
+
yield new_context if block_given?
|
71
|
+
new_context
|
72
|
+
end
|
73
|
+
|
74
|
+
class Context
|
75
|
+
attr_reader :data
|
76
|
+
|
77
|
+
def initialize(data)
|
78
|
+
@data = data
|
79
|
+
end
|
80
|
+
|
81
|
+
def log(obj, args={})
|
82
|
+
Lines.log(obj, args.merge(data))
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
# A backward-compatibile logger
|
87
|
+
def logger
|
88
|
+
@logger ||= (
|
89
|
+
require "lines/logger"
|
90
|
+
Logger.new(self)
|
91
|
+
)
|
92
|
+
end
|
93
|
+
|
94
|
+
protected
|
95
|
+
|
96
|
+
def sanitize_obj(obj, args={})
|
97
|
+
if obj.kind_of?(Exception)
|
98
|
+
ex = obj
|
99
|
+
obj = {ex: ex.class, msg: ex.to_s}
|
100
|
+
if ex.respond_to?(:backtrace) && ex.backtrace
|
101
|
+
obj[:backtrace] = ex.backtrace
|
102
|
+
end
|
103
|
+
elsif !obj.kind_of?(Hash)
|
104
|
+
if obj.respond_to?(:to_h)
|
105
|
+
obj = obj.to_h
|
106
|
+
else
|
107
|
+
obj = {msg: obj}
|
108
|
+
end
|
109
|
+
end
|
110
|
+
obj.merge(args)
|
111
|
+
end
|
112
|
+
|
113
|
+
def to_outputter(out)
|
114
|
+
return out if out.respond_to?(:output)
|
115
|
+
return StreamOutputter.new(out) if out.respond_to?(:write)
|
116
|
+
|
117
|
+
case out
|
118
|
+
when IO
|
119
|
+
StreamOutputter.new(out)
|
120
|
+
when Syslog
|
121
|
+
SyslogOutputter.new
|
122
|
+
else
|
123
|
+
raise ArgumentError, "unknown outputter #{out.inspect}"
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
class StreamOutputter
|
129
|
+
# stream must accept a #write(str) message
|
130
|
+
def initialize(stream = $stderr)
|
131
|
+
@stream = stream
|
132
|
+
# Is this needed ?
|
133
|
+
@stream.sync = true if @stream.respond_to?(:sync)
|
134
|
+
end
|
135
|
+
|
136
|
+
def output(dumper, obj)
|
137
|
+
str = dumper.dump(obj) + NL
|
138
|
+
stream.write str
|
139
|
+
end
|
140
|
+
|
141
|
+
protected
|
142
|
+
|
143
|
+
attr_reader :stream
|
144
|
+
end
|
145
|
+
|
146
|
+
require 'syslog'
|
147
|
+
class SyslogOutputter
|
148
|
+
PRI2SYSLOG = {
|
149
|
+
debug: Syslog::LOG_DEBUG,
|
150
|
+
info: Syslog::LOG_INFO,
|
151
|
+
warn: Syslog::LOG_WARNING,
|
152
|
+
warning: Syslog::LOG_WARNING,
|
153
|
+
err: Syslog::LOG_ERR,
|
154
|
+
error: Syslog::LOG_ERR,
|
155
|
+
crit: Syslog::LOG_CRIT,
|
156
|
+
critical: Syslog::LOG_CRIT,
|
157
|
+
}
|
158
|
+
|
159
|
+
def initialize(syslog = Syslog, app_name=nil)
|
160
|
+
@app_name = app_name
|
161
|
+
@syslog = syslog
|
162
|
+
prepare_syslog
|
163
|
+
end
|
164
|
+
|
165
|
+
def output(dumper, obj)
|
166
|
+
obj = obj.dup
|
167
|
+
obj.delete(:pid) # It's going to be part of the message
|
168
|
+
obj.delete(:at) # Also part of the message
|
169
|
+
obj.delete(:app) # And again
|
170
|
+
|
171
|
+
level = extract_pri(obj)
|
172
|
+
str = dumper.dump(obj)
|
173
|
+
|
174
|
+
syslog.log(level, str)
|
175
|
+
end
|
176
|
+
|
177
|
+
protected
|
178
|
+
|
179
|
+
attr_reader :app_name
|
180
|
+
attr_reader :syslog
|
181
|
+
|
182
|
+
def prepare_syslog
|
183
|
+
unless syslog.opened?
|
184
|
+
# Did you know ? app_name is detected by syslog if nil
|
185
|
+
syslog.open(app_name,
|
186
|
+
Syslog::LOG_PID & Syslog::LOG_CONS & Syslog::LOG_NDELAY,
|
187
|
+
Syslog::LOG_USER)
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
def extract_pri(h)
|
192
|
+
pri = h.delete(:pri).to_s.downcase
|
193
|
+
PRI2SYSLOG[pri] || PRI2SYSLOG[:info]
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
# Some opinions here as well on the format:
|
198
|
+
#
|
199
|
+
# We really want to never fail at dumping because you know, they're logs.
|
200
|
+
# It's better to get a slightly less readable log that no logs at all.
|
201
|
+
#
|
202
|
+
# We're trying to be helpful for humans. It means that if possible we want
|
203
|
+
# to make things shorter and more readable. It also means that ideally
|
204
|
+
# we would like the parsing to be isomorphic but approximations are alright.
|
205
|
+
# For example a symbol might become a string.
|
206
|
+
#
|
207
|
+
# Basically, values are either composite (dictionaries and arrays), quoted
|
208
|
+
# strings or litterals. Litterals are strings that can be parsed to
|
209
|
+
# something else depending if the language supports it or not.
|
210
|
+
# Litterals never contain white-spaces or other weird (very precise !) characters.
|
211
|
+
#
|
212
|
+
# the true litteral is written as "#t"
|
213
|
+
# the false litteral is written as "#f"
|
214
|
+
# the nil / null litteral is written as "nil"
|
215
|
+
#
|
216
|
+
# dictionary keys are always strings or litterals.
|
217
|
+
#
|
218
|
+
# Pleaaase, keep units with numbers. And we provide a way for this:
|
219
|
+
# a tuple of (number, litteral) can be concatenated. Eg: (3, 'ms') => 3ms
|
220
|
+
# alternatively if your language supports a time range it could be serialized
|
221
|
+
# to the same value (and parsed back as well).
|
222
|
+
#
|
223
|
+
# if we don't know how to serialize something we provide a language-specific
|
224
|
+
# string of it and encode is at such.
|
225
|
+
#
|
226
|
+
# The output ought to use the UTF-8 encoding.
|
227
|
+
#
|
228
|
+
# This dumper has been inspired by the OkJSON gem (both formats look alike
|
229
|
+
# after all).
|
230
|
+
class Dumper
|
231
|
+
def dump(obj) #=> String
|
232
|
+
objenc_internal(obj)
|
233
|
+
end
|
234
|
+
|
235
|
+
# Used to introduce new ruby litterals.
|
236
|
+
def map(klass, &rule)
|
237
|
+
@mapping[klass] = rule
|
238
|
+
end
|
239
|
+
|
240
|
+
protected
|
241
|
+
|
242
|
+
attr_reader :mapping
|
243
|
+
|
244
|
+
def initialize
|
245
|
+
@mapping = {}
|
246
|
+
end
|
247
|
+
|
248
|
+
def objenc_internal(x)
|
249
|
+
x.map{|k,v| "#{keyenc(k)}=#{valenc(v)}" }.join(' ')
|
250
|
+
end
|
251
|
+
|
252
|
+
def keyenc(k)
|
253
|
+
case k
|
254
|
+
when String, Symbol then strenc(k)
|
255
|
+
else
|
256
|
+
strenc(k.inspect)
|
257
|
+
end
|
258
|
+
end
|
259
|
+
|
260
|
+
def valenc(x)
|
261
|
+
case x
|
262
|
+
when Hash then objenc(x)
|
263
|
+
when Array then arrenc(x)
|
264
|
+
when String, Symbol then strenc(x)
|
265
|
+
when Numeric then numenc(x)
|
266
|
+
when Time, Date then timeenc(x)
|
267
|
+
when true then "#t"
|
268
|
+
when false then "#f"
|
269
|
+
when nil then "nil"
|
270
|
+
else
|
271
|
+
litenc(x)
|
272
|
+
end
|
273
|
+
end
|
274
|
+
|
275
|
+
def objenc(x)
|
276
|
+
'{' + objenc_internal(x) + '}'
|
277
|
+
end
|
278
|
+
|
279
|
+
def arrenc(a)
|
280
|
+
# num + unit. Eg: 3ms
|
281
|
+
if a.size == 2 && a.first.kind_of?(Numeric) && is_literal?(a.last.to_s)
|
282
|
+
numenc(a.first) + strenc(a.last)
|
283
|
+
else
|
284
|
+
'[' + a.map{|x| valenc(x)}.join(' ') + ']'
|
285
|
+
end
|
286
|
+
end
|
287
|
+
|
288
|
+
# TODO: Single-quote espace if possible
|
289
|
+
def strenc(s)
|
290
|
+
s = s.to_s
|
291
|
+
s = s.inspect unless is_literal?(s)
|
292
|
+
s
|
293
|
+
end
|
294
|
+
|
295
|
+
def numenc(n)
|
296
|
+
#case n
|
297
|
+
# when Float
|
298
|
+
# "%.3f" % n
|
299
|
+
#else
|
300
|
+
n.to_s
|
301
|
+
#end
|
302
|
+
end
|
303
|
+
|
304
|
+
def litenc(x)
|
305
|
+
klass = (x.class.ancestors & mapping.keys).first
|
306
|
+
if klass
|
307
|
+
mapping[klass].call(x)
|
308
|
+
else
|
309
|
+
strenc(x.inspect)
|
310
|
+
end
|
311
|
+
rescue
|
312
|
+
klass = (class << x; self; end).ancestors.first
|
313
|
+
strenc("#<#{klass}:0x#{x.__id__.to_s(16)}>")
|
314
|
+
end
|
315
|
+
|
316
|
+
def timeenc(t)
|
317
|
+
t.iso8601
|
318
|
+
end
|
319
|
+
|
320
|
+
def is_literal?(s)
|
321
|
+
!s.index(/[\s'"]/)
|
322
|
+
end
|
323
|
+
|
324
|
+
end
|
325
|
+
|
326
|
+
require 'securerandom'
|
327
|
+
module UniqueIDs
|
328
|
+
# A small utility to generate unique IDs that are as short as possible.
|
329
|
+
#
|
330
|
+
# It's useful to link contextes together
|
331
|
+
#
|
332
|
+
# See http://preshing.com/20110504/hash-collision-probabilities
|
333
|
+
def id(collision_chance=1.0/10e9, over_x_messages=10e3)
|
334
|
+
# Assuming that the distribution is perfectly random
|
335
|
+
# how many bits do we need so that the chance of collision over_x_messages
|
336
|
+
# is lower thant collision_chance ?
|
337
|
+
number_of_possible_numbers = (over_x_messages ** 2) / (2 * collision_chance)
|
338
|
+
num_bytes = (Math.log2(number_of_possible_numbers) / 8).ceil
|
339
|
+
SecureRandom.urlsafe_base64(num_bytes)
|
340
|
+
end
|
341
|
+
end
|
342
|
+
extend UniqueIDs
|
343
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'active_record'
|
2
|
+
require 'lines'
|
3
|
+
|
4
|
+
module Lines
|
5
|
+
class ActiveRecordSubscriber < ActiveSupport::LogSubscriber
|
6
|
+
def sql(event)
|
7
|
+
payload = event.payload
|
8
|
+
|
9
|
+
return if payload[:name] == "SCHEMA"
|
10
|
+
|
11
|
+
args = {}
|
12
|
+
|
13
|
+
args[:name] = payload[:name] if payload[:name]
|
14
|
+
args[:sql] = payload[:sql].squeeze(' ')
|
15
|
+
|
16
|
+
if payload[:binds] && payload[:binds].any?
|
17
|
+
args[:binds] = payload[:binds].inject({}) do |hash,(col, v)|
|
18
|
+
hash[col.name] = v
|
19
|
+
hash
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
args[:elapsed] = [event.duration, 's']
|
24
|
+
|
25
|
+
Lines.log(args)
|
26
|
+
end
|
27
|
+
|
28
|
+
def identity(event)
|
29
|
+
Lines.log(name: event.payload[:name], line: event.payload[:line])
|
30
|
+
end
|
31
|
+
|
32
|
+
def logger; true; end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
Lines::ActiveRecordSubscriber.attach_to :active_record
|
data/lib/lines/loader.rb
ADDED
@@ -0,0 +1,166 @@
|
|
1
|
+
begin
|
2
|
+
require 'parslet'
|
3
|
+
rescue LoadError
|
4
|
+
warn "lines/loader depends on parslet"
|
5
|
+
raise
|
6
|
+
end
|
7
|
+
|
8
|
+
# http://zerowidth.com/2013/02/24/parsing-toml-in-ruby-with-parslet.html
|
9
|
+
module Lines
|
10
|
+
module Error; end
|
11
|
+
module ParseError; include Error; end
|
12
|
+
module Loader; extend self
|
13
|
+
def load(s)
|
14
|
+
parser = Parser.new
|
15
|
+
transformer = Transformer.new
|
16
|
+
|
17
|
+
tree = parser.parse(s)
|
18
|
+
#puts; p tree; puts
|
19
|
+
transformer.apply(tree)
|
20
|
+
rescue Parslet::ParseFailed => ex
|
21
|
+
# Mark as being part of the Lines library
|
22
|
+
ex.extend ParseError
|
23
|
+
raise
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# Mostly copied over from the JSON example:
|
28
|
+
# https://github.com/kschiess/parslet/blob/master/example/json.rb
|
29
|
+
#
|
30
|
+
# TODO:
|
31
|
+
# ISO8601 dates
|
32
|
+
class Parser < Parslet::Parser
|
33
|
+
|
34
|
+
rule(:spaces) { match(' ').repeat(1) }
|
35
|
+
rule(:spaces?) { spaces.maybe }
|
36
|
+
|
37
|
+
rule(:digit) { match['0-9'] }
|
38
|
+
|
39
|
+
rule(:number) {
|
40
|
+
(
|
41
|
+
str('-').maybe >> (
|
42
|
+
str('0') | (match['1-9'] >> digit.repeat)
|
43
|
+
) >> (
|
44
|
+
str('.') >> digit.repeat(1)
|
45
|
+
).maybe >> (
|
46
|
+
match('[eE]') >> (str('+') | str('-')).maybe >> digit.repeat(1)
|
47
|
+
).maybe
|
48
|
+
).as(:number)
|
49
|
+
}
|
50
|
+
|
51
|
+
rule(:time) {
|
52
|
+
digit.repeat(4) >> str('-') >>
|
53
|
+
digit.repeat(2) >> str('-') >>
|
54
|
+
digit.repeat(2) >> str('T') >>
|
55
|
+
digit.repeat(2) >> str(':') >>
|
56
|
+
digit.repeat(2) >> str(':') >>
|
57
|
+
digit.repeat(2) >> str('Z')
|
58
|
+
}
|
59
|
+
|
60
|
+
rule(:singlequoted_string) {
|
61
|
+
str("'") >> (
|
62
|
+
str('\\') >> any | str("'").absent? >> any
|
63
|
+
).repeat.as(:string) >> str("'")
|
64
|
+
}
|
65
|
+
|
66
|
+
rule(:doublequoted_string) {
|
67
|
+
str('"') >> (
|
68
|
+
str('\\') >> any | str('"').absent? >> any
|
69
|
+
).repeat.as(:string) >> str('"')
|
70
|
+
}
|
71
|
+
|
72
|
+
rule(:simple_string) {
|
73
|
+
match['a-zA-Z_\-:'].repeat.as(:string)
|
74
|
+
}
|
75
|
+
|
76
|
+
rule(:string) {
|
77
|
+
singlequoted_string | doublequoted_string | simple_string
|
78
|
+
}
|
79
|
+
|
80
|
+
rule(:array) {
|
81
|
+
str('[') >> spaces? >>
|
82
|
+
(value >> (spaces >> value).repeat).maybe.as(:array) >>
|
83
|
+
spaces? >> str(']')
|
84
|
+
}
|
85
|
+
|
86
|
+
rule(:object) {
|
87
|
+
str('{') >> spaces? >>
|
88
|
+
(entry >> (spaces >> entry).repeat).maybe.as(:object) >>
|
89
|
+
spaces? >> str('}')
|
90
|
+
}
|
91
|
+
|
92
|
+
rule(:key) {
|
93
|
+
match['a-zA-Z0-9_'].repeat
|
94
|
+
}
|
95
|
+
|
96
|
+
rule(:value) {
|
97
|
+
str('#t').as(:true) | str('#f').as(:false) |
|
98
|
+
str('nil').as(:nil) |
|
99
|
+
object | array |
|
100
|
+
number | time |
|
101
|
+
string
|
102
|
+
}
|
103
|
+
|
104
|
+
rule(:entry) {
|
105
|
+
(
|
106
|
+
key.as(:key) >>
|
107
|
+
str('=') >>
|
108
|
+
value.as(:val)
|
109
|
+
).as(:entry)
|
110
|
+
}
|
111
|
+
|
112
|
+
rule(:top) { spaces? >> (entry >> (spaces >> entry).repeat).maybe.as(:object) >> spaces? }
|
113
|
+
#rule(:top) { (digit >> digit).as(:digit) }
|
114
|
+
#rule(:top) { time }
|
115
|
+
|
116
|
+
root(:top)
|
117
|
+
end
|
118
|
+
|
119
|
+
class Transformer < Parslet::Transform
|
120
|
+
|
121
|
+
class Entry < Struct.new(:key, :val); end
|
122
|
+
|
123
|
+
rule(array: subtree(:ar)) {
|
124
|
+
case ar
|
125
|
+
when nil
|
126
|
+
[]
|
127
|
+
when Array
|
128
|
+
ar
|
129
|
+
else
|
130
|
+
[ar]
|
131
|
+
end
|
132
|
+
}
|
133
|
+
rule(object: subtree(:ob)) {
|
134
|
+
case ob
|
135
|
+
when nil
|
136
|
+
[]
|
137
|
+
when Array
|
138
|
+
ob
|
139
|
+
else
|
140
|
+
[ob]
|
141
|
+
end.inject({}) { |h, e|
|
142
|
+
h[e[:entry][:key].to_s] = e[:entry][:val]; h
|
143
|
+
}
|
144
|
+
}
|
145
|
+
|
146
|
+
# rule(entry: { key: simple(:ke), val: simple(:va) }) {
|
147
|
+
# Entry.new(ke.to_s, va)
|
148
|
+
# }
|
149
|
+
|
150
|
+
rule(time: { year: simple(:ye), month: simple(:mo), day: simple(:da), hour: simple(:ho), minute: simple(:min), second: simple(:sec)}) {
|
151
|
+
Time.new(ye.to_i, mo.to_i, da.to_i, ho.to_i, min.to_i, sec.to_i, "+00:00")
|
152
|
+
}
|
153
|
+
|
154
|
+
rule(string: simple(:st)) {
|
155
|
+
st.to_s
|
156
|
+
}
|
157
|
+
|
158
|
+
rule(number: simple(:nb)) {
|
159
|
+
nb.match(/[eE\.]/) ? Float(nb) : Integer(nb)
|
160
|
+
}
|
161
|
+
|
162
|
+
rule(nil: simple(:ni)) { nil }
|
163
|
+
rule(true: simple(:tr)) { true }
|
164
|
+
rule(false: simple(:fa)) { false }
|
165
|
+
end
|
166
|
+
end
|
data/lib/lines/logger.rb
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
module Lines
|
2
|
+
# Backward-compatible logger
|
3
|
+
# http://ruby-doc.org/stdlib-2.0/libdoc/logger/rdoc/Logger.html#method-i-log
|
4
|
+
class Logger
|
5
|
+
LEVELS = {
|
6
|
+
0 => :debug,
|
7
|
+
1 => :info,
|
8
|
+
2 => :warn,
|
9
|
+
3 => :error,
|
10
|
+
4 => :fatal,
|
11
|
+
5 => :unknown,
|
12
|
+
}
|
13
|
+
|
14
|
+
def initialize(line)
|
15
|
+
@line = line
|
16
|
+
end
|
17
|
+
|
18
|
+
def log(severity, message = nil, progname = nil, &block)
|
19
|
+
pri = LEVELS[severity] || severity
|
20
|
+
if block_given?
|
21
|
+
progname = message
|
22
|
+
message = yield.to_s rescue $!.to_s
|
23
|
+
end
|
24
|
+
|
25
|
+
data = { pri: pri }
|
26
|
+
data[:app] = progname if progname
|
27
|
+
data[:msg] = message if message
|
28
|
+
|
29
|
+
@line.log(data)
|
30
|
+
end
|
31
|
+
|
32
|
+
LEVELS.values.each do |level|
|
33
|
+
define_method(level) do |message=nil, &block|
|
34
|
+
log(level, message, &block)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
alias << info
|
39
|
+
alias unknown info
|
40
|
+
|
41
|
+
def noop(*a); true end
|
42
|
+
%w[add
|
43
|
+
clone
|
44
|
+
datetime_format
|
45
|
+
datetime_format=
|
46
|
+
debug?
|
47
|
+
info?
|
48
|
+
error?
|
49
|
+
fatal?
|
50
|
+
warn?
|
51
|
+
level
|
52
|
+
level=
|
53
|
+
progname
|
54
|
+
progname=
|
55
|
+
sev_threshold
|
56
|
+
sev_threshold=
|
57
|
+
].each do |op|
|
58
|
+
alias_method(op, :noop)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'rack/commonlogger'
|
2
|
+
require 'lines'
|
3
|
+
|
4
|
+
module Lines
|
5
|
+
class RackLogger < Rack::CommonLogger
|
6
|
+
# In development mode the common logger is always inserted
|
7
|
+
def self.silence_common_logger!
|
8
|
+
Rack::CommonLogger.module_eval("def call(env); @app.call(env); end")
|
9
|
+
end
|
10
|
+
|
11
|
+
def initialize(app)
|
12
|
+
@app = app
|
13
|
+
end
|
14
|
+
|
15
|
+
def call(env)
|
16
|
+
began_at = Time.now
|
17
|
+
status, header, body = @app.call(env)
|
18
|
+
header = Utils::HeaderHash.new(header)
|
19
|
+
body = BodyProxy.new(body) { log(env, status, header, began_at) }
|
20
|
+
[status, header, body]
|
21
|
+
end
|
22
|
+
|
23
|
+
protected
|
24
|
+
|
25
|
+
def log(env, status, header, began_at)
|
26
|
+
Lines.log(
|
27
|
+
remote_addr: env['HTTP_X_FORWARDED_FOR'] || env["REMOTE_ADDR"],
|
28
|
+
remote_user: env['REMOTE_USER'] || '',
|
29
|
+
method: env['REQUEST_METHOD'],
|
30
|
+
path: env['PATH_INFO'],
|
31
|
+
query: env["QUERY_STRING"],
|
32
|
+
status: status.to_s[0..3],
|
33
|
+
length: extract_content_length(header),
|
34
|
+
elapsed: [Time.now - began_at, 's'],
|
35
|
+
)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
metadata
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: lines
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Jonas Pfenniger
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2013-04-03 00:00:00.000000000 Z
|
13
|
+
dependencies: []
|
14
|
+
description: Lines is a cross-language logging format
|
15
|
+
email: jonas@pfenniger.name
|
16
|
+
executables: []
|
17
|
+
extensions: []
|
18
|
+
extra_rdoc_files: []
|
19
|
+
files:
|
20
|
+
- README.md
|
21
|
+
- lib/lines/active_record.rb
|
22
|
+
- lib/lines/loader.rb
|
23
|
+
- lib/lines/logger.rb
|
24
|
+
- lib/lines/rack_logger.rb
|
25
|
+
- lib/lines/version.rb
|
26
|
+
- lib/lines.rb
|
27
|
+
homepage: https://github.com/zimbatm/lines
|
28
|
+
licenses: []
|
29
|
+
post_install_message:
|
30
|
+
rdoc_options: []
|
31
|
+
require_paths:
|
32
|
+
- lib
|
33
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
34
|
+
none: false
|
35
|
+
requirements:
|
36
|
+
- - ! '>='
|
37
|
+
- !ruby/object:Gem::Version
|
38
|
+
version: '0'
|
39
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
40
|
+
none: false
|
41
|
+
requirements:
|
42
|
+
- - ! '>='
|
43
|
+
- !ruby/object:Gem::Version
|
44
|
+
version: '0'
|
45
|
+
requirements: []
|
46
|
+
rubyforge_project:
|
47
|
+
rubygems_version: 1.8.23
|
48
|
+
signing_key:
|
49
|
+
specification_version: 3
|
50
|
+
summary: Logging revisited
|
51
|
+
test_files: []
|