lines 0.1.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.
- 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: []
|