/
diagnostic_context.rb
452 lines (401 loc) · 14.4 KB
/
diagnostic_context.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
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
module Logging
# A Mapped Diagnostic Context, or MDC in short, is an instrument used to
# distinguish interleaved log output from different sources. Log output is
# typically interleaved when a server handles multiple clients
# near-simultaneously.
#
# Interleaved log output can still be meaningful if each log entry from
# different contexts had a distinctive stamp. This is where MDCs come into
# play.
#
# The MDC provides a hash of contextual messages that are identified by
# unique keys. These unique keys are set by the application and appended
# to log messages to identify groups of log events. One use of the Mapped
# Diagnostic Context is to store HTTP request headers associated with a Rack
# request. These headers can be included with all log messages emitted while
# generating the HTTP response.
#
# When configured to do so, PatternLayout instances will automatically
# retrieve the mapped diagnostic context for the current thread with out any
# user intervention. This context information can be used to track user
# sessions in a Rails application, for example.
#
# Note that MDCs are managed on a per thread basis. MDC operations such as
# `[]`, `[]=`, and `clear` affect the MDC of the current thread only. MDCs
# of other threads remain unaffected.
#
# By default, when a new thread is created it will inherit the context of
# its parent thread. However, the `inherit` method may be used to inherit
# context for any other thread in the application.
#
module MappedDiagnosticContext
extend self
# The name used to retrieve the MDC from thread-local storage.
NAME = :logging_mapped_diagnostic_context
# The name used to retrieve the MDC stack from thread-local storage.
STACK_NAME = :logging_mapped_diagnostic_context_stack
# Public: Put a context value as identified with the key parameter into
# the current thread's context map.
#
# key - The String identifier for the context.
# value - The String value to store.
#
# Returns the value.
#
def []=( key, value )
clear_context
peek.store(key.to_s, value)
end
# Public: Get the context value identified with the key parameter.
#
# key - The String identifier for the context.
#
# Returns the value associated with the key or nil if there is no value
# present.
#
def []( key )
context.fetch(key.to_s, nil)
end
# Public: Remove the context value identified with the key parameter.
#
# key - The String identifier for the context.
#
# Returns the value associated with the key or nil if there is no value
# present.
#
def delete( key )
clear_context
peek.delete(key.to_s)
end
# Public: Add all the key/value pairs from the given hash to the current
# mapped diagnostic context. The keys will be converted to strings.
# Existing keys of the same name will be overwritten.
#
# hash - The Hash of values to add to the current context.
#
# Returns this context.
#
def update( hash )
clear_context
sanitize(hash, peek)
self
end
# Public: Push a new Hash of key/value pairs onto the stack of contexts.
#
# hash - The Hash of values to push onto the context stack.
#
# Returns this context.
# Raises an ArgumentError if hash is not a Hash.
#
def push( hash )
clear_context
stack << sanitize(hash)
self
end
# Public: Remove the most recently pushed Hash from the stack of contexts.
# If no contexts have been pushed then no action will be taken. The
# default context cannot be popped off the stack; please use the `clear`
# method if you want to remove all key/value pairs from the context.
#
# Returns nil or the Hash removed from the stack.
#
def pop
return unless Thread.current[STACK_NAME]
return unless stack.length > 1
clear_context
stack.pop
end
# Public: Clear all mapped diagnostic information if any. This method is
# useful in cases where the same thread can be potentially used over and
# over in different unrelated contexts.
#
# Returns the MappedDiagnosticContext.
#
def clear
clear_context
Thread.current[STACK_NAME] = nil
self
end
# Public: Inherit the diagnostic context of another thread. In the vast
# majority of cases the other thread will the parent that spawned the
# current thread. The diagnostic context from the parent thread is cloned
# before being inherited; the two diagnostic contexts can be changed
# independently.
#
# Returns the MappedDiagnosticContext.
#
def inherit( obj )
case obj
when Hash
Thread.current[STACK_NAME] = [obj.dup]
when Thread
return if Thread.current == obj
Thread.exclusive {
if obj[STACK_NAME]
hash = flatten(obj[STACK_NAME])
Thread.current[STACK_NAME] = [hash]
end
}
end
self
end
# Returns the Hash acting as the storage for this MappedDiagnosticContext.
# A new storage Hash is created for each Thread running in the
# application.
#
def context
c = Thread.current[NAME]
return c unless c.nil?
return Thread.current[NAME] = {} unless Thread.current[STACK_NAME]
Thread.current[NAME] = flatten(stack)
end
# Returns the stack of Hash objects that are storing the diagnostic
# context information. This stack is guarnteed to always contain at least
# one Hash.
#
def stack
Thread.current[STACK_NAME] ||= [{}]
end
# Returns the most current Hash from the stack of contexts.
#
def peek
stack.last
end
# Remove the flattened context.
#
def clear_context
Thread.current[NAME] = nil
self
end
# Given a Hash convert all keys into Strings. The values are not altered
# in any way. The converted keys and their values are stored in the target
# Hash if provided. Otherwise a new Hash is created and returned.
#
# hash - The Hash of values to push onto the context stack.
# target - The target Hash to store the key value pairs.
#
# Returns a new Hash with all keys converted to Strings.
# Raises an ArgumentError if hash is not a Hash.
#
def sanitize( hash, target = {} )
unless Hash === hash
raise ArgumentError, "Expecting a Hash but received a #{hash.class.name}"
end
hash.each { |k,v| target[k.to_s] = v }
return target
end
# Given an Array of Hash objects, flatten all the key/value pairs from the
# Hash objects in the ary into a single Hash. The flattening occurs left
# to right. So that the key/value in the very last Hash overrides any
# other key from the previous Hash objcts.
#
# ary - An Array of Hash objects.
#
# Returns a Hash.
#
def flatten( ary )
return ary.first.dup if ary.length == 1
hash = {}
ary.each { |h| hash.update h }
return hash
end
end # MappedDiagnosticContext
# A Nested Diagnostic Context, or NDC in short, is an instrument to
# distinguish interleaved log output from different sources. Log output is
# typically interleaved when a server handles multiple clients
# near-simultaneously.
#
# Interleaved log output can still be meaningful if each log entry from
# different contexts had a distinctive stamp. This is where NDCs come into
# play.
#
# The NDC is a stack of contextual messages that are pushed and popped by
# the client as different contexts are encountered in the application. When a
# new context is entered, the client will `push` a new message onto the NDC
# stack. This message appears in all log messages. When this context is
# exited, the client will call `pop` to remove the message.
#
# * Contexts can be nested
# * When entering a context, call `Logging.ndc.push`
# * When leaving a context, call `Logging.ndc.pop`
# * Configure the PatternLayout to log context information
#
# There is no penalty for forgetting to match each push operation with a
# corresponding pop, except the obvious mismatch between the real
# application context and the context set in the NDC.
#
# When configured to do so, PatternLayout instance will automatically
# retrieve the nested diagnostic context for the current thread with out any
# user intervention. This context information can be used to track user
# sessions in a Rails application, for example.
#
# Note that NDCs are managed on a per thread basis. NDC operations such as
# `push`, `pop`, and `clear` affect the NDC of the current thread only. NDCs
# of other threads remain unaffected.
#
# By default, when a new thread is created it will inherit the context of
# its parent thread. However, the `inherit` method may be used to inherit
# context for any other thread in the application.
#
module NestedDiagnosticContext
extend self
# The name used to retrieve the NDC from thread-local storage.
NAME = :logging_nested_diagnostic_context
# Public: Push new diagnostic context information for the current thread.
# The contents of the message parameter is determined solely by the
# client.
#
# message - The message String to add to the current context.
#
# Returns the current NestedDiagnosticContext.
#
def push( message )
context.push(message)
if block_given?
begin
yield
ensure
context.pop
end
end
self
end
alias_method :<<, :push
# Public: Clients should call this method before leaving a diagnostic
# context. The returned value is the last pushed message. If no
# context is available then `nil` is returned.
#
# Returns the last pushed diagnostic message String or nil if no messages
# exist.
#
def pop
context.pop
end
# Public: Looks at the last diagnostic context at the top of this NDC
# without removing it. The returned value is the last pushed message. If
# no context is available then `nil` is returned.
#
# Returns the last pushed diagnostic message String or nil if no messages
# exist.
#
def peek
context.last
end
# Public: Clear all nested diagnostic information if any. This method is
# useful in cases where the same thread can be potentially used over and
# over in different unrelated contexts.
#
# Returns the NestedDiagnosticContext.
#
def clear
Thread.current[NAME] = nil
self
end
# Public: Inherit the diagnostic context of another thread. In the vast
# majority of cases the other thread will the parent that spawned the
# current thread. The diagnostic context from the parent thread is cloned
# before being inherited; the two diagnostic contexts can be changed
# independently.
#
# Returns the NestedDiagnosticContext.
#
def inherit( obj )
case obj
when Array
Thread.current[NAME] = obj.dup
when Thread
return if Thread.current == obj
Thread.exclusive {
Thread.current[NAME] = obj[NAME].dup if obj[NAME]
}
end
self
end
# Returns the Array acting as the storage stack for this
# NestedDiagnosticContext. A new storage Array is created for each Thread
# running in the application.
#
def context
Thread.current[NAME] ||= Array.new
end
end # NestedDiagnosticContext
# Public: Accessor method for getting the current Thread's
# MappedDiagnosticContext.
#
# Returns MappedDiagnosticContext
#
def self.mdc() MappedDiagnosticContext end
# Public: Accessor method for getting the current Thread's
# NestedDiagnosticContext.
#
# Returns NestedDiagnosticContext
#
def self.ndc() NestedDiagnosticContext end
# Public: Convenience method that will clear both the Mapped Diagnostic
# Context and the Nested Diagnostic Context of the current thread. If the
# `all` flag passed to this method is true, then the diagnostic contexts for
# _every_ thread in the application will be cleared.
#
# all - Boolean flag used to clear the context of every Thread (default is false)
#
# Returns the Logging module.
#
def self.clear_diagnostic_contexts( all = false )
if all
Thread.exclusive {
Thread.list.each { |thread|
thread[MappedDiagnosticContext::NAME] = nil if thread[MappedDiagnosticContext::NAME]
thread[NestedDiagnosticContext::NAME] = nil if thread[NestedDiagnosticContext::NAME]
thread[MappedDiagnosticContext::STACK_NAME] = nil if thread[MappedDiagnosticContext::STACK_NAME]
}
}
else
MappedDiagnosticContext.clear
NestedDiagnosticContext.clear
end
self
end
end # module Logging
# :stopdoc:
class Thread
class << self
%w[new start fork].each do |m|
class_eval <<-__, __FILE__, __LINE__
alias_method :_orig_#{m}, :#{m}
private :_orig_#{m}
def #{m}( *a, &b )
create_with_logging_context(:_orig_#{m}, *a ,&b)
end
__
end
private
# In order for the diagnostic contexts to behave properly we need to
# inherit state from the parent thread. The only way I have found to do
# this in Ruby is to override `new` and capture the contexts from the
# parent Thread at the time the child Thread is created. The code below does
# just this. If there is a more idiomatic way of accomplishing this in Ruby,
# please let me know!
#
# Also, great care is taken in this code to ensure that a reference to the
# parent thread does not exist in the binding associated with the block
# being executed in the child thread. The same is true for the parent
# thread's mdc and ndc. If any of those references end up in the binding,
# then they cannot be garbage collected until the child thread exits.
#
def create_with_logging_context( m, *a, &b )
mdc, ndc = nil
if Thread.current[Logging::MappedDiagnosticContext::STACK_NAME]
mdc = Logging::MappedDiagnosticContext.context.dup
end
if Thread.current[Logging::NestedDiagnosticContext::NAME]
ndc = Logging::NestedDiagnosticContext.context.dup
end
self.send(m, *a) { |*args|
Logging::MappedDiagnosticContext.inherit(mdc)
Logging::NestedDiagnosticContext.inherit(ndc)
b.call(*args)
}
end
end
end # Thread
# :startdoc: