-
-
Notifications
You must be signed in to change notification settings - Fork 542
/
rack_logger.rb
187 lines (154 loc) · 4.61 KB
/
rack_logger.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
# frozen_string_literal: true
require "delegate"
require "json"
module Hanami
# @api private
module Web
# Rack logger for Hanami apps
#
# @api private
# @since 2.0.0
class RackLogger
EMPTY_PARAMS = {}.freeze
private_constant :EMPTY_PARAMS
REQUEST_METHOD = "REQUEST_METHOD"
private_constant :REQUEST_METHOD
HTTP_X_FORWARDED_FOR = "HTTP_X_FORWARDED_FOR"
private_constant :HTTP_X_FORWARDED_FOR
REMOTE_ADDR = "REMOTE_ADDR"
private_constant :REMOTE_ADDR
SCRIPT_NAME = "SCRIPT_NAME"
private_constant :SCRIPT_NAME
PATH_INFO = "PATH_INFO"
private_constant :PATH_INFO
ROUTER_PARAMS = "router.params"
private_constant :ROUTER_PARAMS
CONTENT_LENGTH = "CONTENT_LENGTH"
private_constant :CONTENT_LENGTH
MILLISECOND = "ms"
private_constant :MILLISECOND
MICROSECOND = "µs"
private_constant :MICROSECOND
# Dynamic extension used in development and test environments
# @api private
module Development
private
# @since 2.0.0
# @api private
def data(env, status:, elapsed:)
payload = super
payload.delete(:elapsed_unit)
payload[:elapsed] = elapsed > 1000 ? "#{elapsed / 1000}ms" : "#{elapsed}#{MICROSECOND}"
payload
end
end
# @since 2.1.0
# @api private
class UniversalLogger
class << self
# @since 2.1.0
# @api private
def call(logger)
return logger if compatible_logger?(logger)
new(logger)
end
# @since 2.1.0
# @api private
alias_method :[], :call
private
def compatible_logger?(logger)
logger.respond_to?(:tagged) && accepts_entry_payload?(logger)
end
def accepts_entry_payload?(logger)
logger.method(:info).parameters.any? { |(type, _)| type == :keyrest }
end
end
# @since 2.1.0
# @api private
attr_reader :logger
# @since 2.1.0
# @api private
def initialize(logger)
@logger = logger
end
# @since 2.1.0
# @api private
def tagged(*, &blk)
blk.call
end
# Logs the entry as JSON.
#
# This ensures a reasonable (and parseable) representation of our log payload structures for
# loggers that are configured to wholly replace Hanami's default logger.
#
# @since 2.1.0
# @api private
def info(message = nil, **payload)
payload[:message] = message if message
logger.info(JSON.fast_generate(payload))
end
# @see info
#
# @since 2.1.0
# @api private
def error(message = nil, **payload)
payload[:message] = message if message
logger.info(JSON.fast_generate(payload))
end
end
# @api private
# @since 2.0.0
def initialize(logger, env: :development)
@logger = UniversalLogger[logger]
extend(Development) if %i[development test].include?(env)
end
# @api private
# @since 2.0.0
def attach(rack_monitor)
rack_monitor.on :stop do |event|
log_request event[:env], event[:status], event[:time]
end
rack_monitor.on :error do |event|
# TODO: why we don't provide time on error?
log_exception event[:env], event[:exception], 500, 0
end
end
# @api private
# @since 2.0.0
def log_request(env, status, elapsed)
logger.tagged(:rack) do
logger.info(**data(env, status: status, elapsed: elapsed))
end
end
# @api private
# @since 2.0.0
def log_exception(env, exception, status, elapsed)
logger.tagged(:rack) do
logger.error(exception, **data(env, status: status, elapsed: elapsed))
end
end
private
attr_reader :logger
# @api private
# @since 2.0.0
def data(env, status:, elapsed:)
{
verb: env[REQUEST_METHOD],
status: status,
ip: env[HTTP_X_FORWARDED_FOR] || env[REMOTE_ADDR],
path: "#{env[SCRIPT_NAME]}#{env[PATH_INFO]}",
length: extract_content_length(env),
params: env.fetch(ROUTER_PARAMS, EMPTY_PARAMS),
elapsed: elapsed,
elapsed_unit: MICROSECOND,
}
end
# @api private
# @since 2.0.0
def extract_content_length(env)
value = env[CONTENT_LENGTH]
!value || value.to_s == "0" ? "-" : value
end
end
end
end