File: diagnostic_context.rb

package info (click to toggle)
ruby-logging 2.2.2-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 660 kB
  • sloc: ruby: 6,139; sh: 11; makefile: 2
file content (482 lines) | stat: -rw-r--r-- 15,981 bytes parent folder | download | duplicates (2)
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
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482

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.thread_variable_get(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.thread_variable_set(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.thread_variable_set(STACK_NAME, [obj.dup])
      when Thread
        return if Thread.current == obj
        DIAGNOSTIC_MUTEX.synchronize do
          if hash = obj.thread_variable_get(STACK_NAME)
            Thread.current.thread_variable_set(STACK_NAME, [flatten(hash)])
          end
        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.thread_variable_get(NAME)

      if c.nil?
        c = if Thread.current.thread_variable_get(STACK_NAME)
          flatten(stack)
        else
          Hash.new
        end
        Thread.current.thread_variable_set(NAME, c)
      end

      return c
    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
      s = Thread.current.thread_variable_get(STACK_NAME)
      if s.nil?
        s = [{}]
        Thread.current.thread_variable_set(STACK_NAME, s)
      end
      return s
    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.thread_variable_set(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.is_a?(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.thread_variable_set(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.thread_variable_set(NAME, obj.dup)
      when Thread
        return if Thread.current == obj
        DIAGNOSTIC_MUTEX.synchronize do
          Thread.current.thread_variable_set(NAME, obj.thread_variable_get(NAME).dup) if obj.thread_variable_get(NAME)
        end
      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
      c = Thread.current.thread_variable_get(NAME)
      if c.nil?
        c = Array.new
        Thread.current.thread_variable_set(NAME, c)
      end
      return c
    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
      DIAGNOSTIC_MUTEX.synchronize do
        Thread.list.each do |t|
          t.thread_variable_set(MappedDiagnosticContext::NAME, nil)       if t.thread_variable?(MappedDiagnosticContext::NAME)
          t.thread_variable_set(NestedDiagnosticContext::NAME, nil)       if t.thread_variable?(NestedDiagnosticContext::NAME)
          t.thread_variable_set(MappedDiagnosticContext::STACK_NAME, nil) if t.thread_variable?(MappedDiagnosticContext::STACK_NAME)
        end
      end
    else
      MappedDiagnosticContext.clear
      NestedDiagnosticContext.clear
    end

    self
  end

  DIAGNOSTIC_MUTEX = Mutex.new
end

# :stopdoc:
Logging::INHERIT_CONTEXT =
  if ENV.key?("LOGGING_INHERIT_CONTEXT")
    case ENV["LOGGING_INHERIT_CONTEXT"].downcase
    when 'false', 'no', '0'; false
    when false, nil; false
    else true end
  else
    true
  end

if Logging::INHERIT_CONTEXT
  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.thread_variable_get(Logging::MappedDiagnosticContext::STACK_NAME)
          mdc = Logging::MappedDiagnosticContext.context.dup
        end

        if Thread.current.thread_variable_get(Logging::NestedDiagnosticContext::NAME)
          ndc = Logging::NestedDiagnosticContext.context.dup
        end

        # This calls the actual `Thread#new` method to create the Thread instance.
        # If your memory profiling tool says this method is leaking memory, then
        # you are leaking Thread instances somewhere.
        self.send(m, *a) { |*args|
          Logging::MappedDiagnosticContext.inherit(mdc)
          Logging::NestedDiagnosticContext.inherit(ndc)
          b.call(*args)
        }
      end

    end
  end
end
# :startdoc: