File: log.rb

package info (click to toggle)
puppet-agent 7.23.0-1
  • links: PTS, VCS
  • area: main
  • in suites: bookworm
  • size: 19,092 kB
  • sloc: ruby: 245,074; sh: 456; makefile: 38; xml: 33
file content (427 lines) | stat: -rw-r--r-- 12,583 bytes parent folder | download
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
require_relative '../../puppet/util/tagging'
require_relative '../../puppet/util/classgen'
require_relative '../../puppet/util/psych_support'
require_relative '../../puppet/network/format_support'

# Pass feedback to the user.  Log levels are modeled after syslog's, and it is
# expected that that will be the most common log destination.  Supports
# multiple destinations, one of which is a remote server.
class Puppet::Util::Log
  include Puppet::Util
  extend Puppet::Util::ClassGen
  include Puppet::Util::PsychSupport
  include Puppet::Util::Tagging
  include Puppet::Network::FormatSupport

  @levels = [:debug,:info,:notice,:warning,:err,:alert,:emerg,:crit]
  @loglevel = 2

  @desttypes = {}

  # Create a new destination type.
  def self.newdesttype(name, options = {}, &block)

    dest = genclass(
      name,
      :parent     => Puppet::Util::Log::Destination,
      :prefix     => "Dest",
      :block      => block,
      :hash       => @desttypes,
      :attributes => options
    )
    dest.match(dest.name)

    dest
  end

  require_relative 'log/destination'
  require_relative 'log/destinations'

  @destinations = {}

  @queued = []

  class << self
    include Puppet::Util
    include Puppet::Util::ClassGen

    attr_reader :desttypes
  end

  # Reset log to basics.  Basically just flushes and closes files and
  # undefs other objects.
  def Log.close(destination)
    if @destinations.include?(destination)
      @destinations[destination].flush if @destinations[destination].respond_to?(:flush)
      @destinations[destination].close if @destinations[destination].respond_to?(:close)
      @destinations.delete(destination)
    end
  end

  def self.close_all
    @destinations.keys.each { |dest|
      close(dest)
    }
    #TRANSLATORS "Log.close_all" is a method name and should not be translated
    raise Puppet::DevError.new(_("Log.close_all failed to close %{destinations}") % { destinations: @destinations.keys.inspect }) if !@destinations.empty?
  end

  # Flush any log destinations that support such operations.
  def Log.flush
    @destinations.each { |type, dest|
      dest.flush if dest.respond_to?(:flush)
    }
  end

  def Log.autoflush=(v)
    @destinations.each do |type, dest|
      dest.autoflush = v if dest.respond_to?(:autoflush=)
    end
  end

  # Create a new log message.  The primary role of this method is to
  # avoid creating log messages below the loglevel.
  def Log.create(hash)
    raise Puppet::DevError, _("Logs require a level") unless hash.include?(:level)
    raise Puppet::DevError, _("Invalid log level %{level}") % { level: hash[:level] } unless @levels.index(hash[:level])
    @levels.index(hash[:level]) >= @loglevel ? Puppet::Util::Log.new(hash) : nil
  end

  def Log.destinations
    @destinations
  end

  # Yield each valid level in turn
  def Log.eachlevel
    @levels.each { |level| yield level }
  end

  # Return the current log level.
  def Log.level
    @levels[@loglevel]
  end

  # Set the current log level.
  def Log.level=(level)
    level = level.intern unless level.is_a?(Symbol)

    # loglevel is a 0-based index
    loglevel = @levels.index(level)
    raise Puppet::DevError, _("Invalid loglevel %{level}") % { level: level } unless loglevel

    return if @loglevel == loglevel

    # loglevel changed
    @loglevel = loglevel

    # Enable or disable Facter debugging
    Puppet.runtime[:facter].debugging(level == :debug)
  end

  def Log.levels
    @levels.dup
  end

  # Create a new log destination.
  def Log.newdestination(dest)
    # Each destination can only occur once.
    if @destinations.find { |name, obj| obj.name == dest }
      return
    end

    _, type = @desttypes.find do |name, klass|
      klass.match?(dest)
    end

    if type.respond_to?(:suitable?) and not type.suitable?(dest)
      return
    end

    raise Puppet::DevError, _("Unknown destination type %{dest}") % { dest: dest} unless type

    begin
      if type.instance_method(:initialize).arity == 1
        @destinations[dest] = type.new(dest)
      else
        @destinations[dest] = type.new
      end
      flushqueue
      @destinations[dest]
    rescue => detail
      Puppet.log_exception(detail)

      # If this was our only destination, then add the console back in.
      if destinations.empty? && dest.intern != :console
        newdestination(:console)
      end

      # Re-raise (end exit Puppet) because we could not set up logging correctly.
      raise detail
    end
  end

  def Log.with_destination(destination, &block)
    if @destinations.include?(destination)
      yield
    else
      newdestination(destination)
      begin
        yield
      ensure
        close(destination)
      end
    end
  end

  def Log.coerce_string(str)
    return Puppet::Util::CharacterEncoding.convert_to_utf_8(str) if str.valid_encoding?

    # We only select the last 10 callers in the stack to avoid being spammy
    message = _("Received a Log attribute with invalid encoding:%{log_message}") %
        { log_message: Puppet::Util::CharacterEncoding.convert_to_utf_8(str.dump)}
    message += '\n' + _("Backtrace:\n%{backtrace}") % { backtrace: caller(1, 10).join("\n") }
    message
  end
  private_class_method :coerce_string

  # Route the actual message. FIXME There are lots of things this method
  # should do, like caching and a bit more.  It's worth noting that there's
  # a potential for a loop here, if the machine somehow gets the destination set as
  # itself.
  def Log.newmessage(msg)
    return if @levels.index(msg.level) < @loglevel

    msg.message = coerce_string(msg.message)
    msg.source = coerce_string(msg.source)

    queuemessage(msg) if @destinations.length == 0

    @destinations.each do |name, dest|
      dest.handle(msg)
    end
  end

  def Log.queuemessage(msg)
    @queued.push(msg)
  end

  def Log.flushqueue
    return unless @destinations.size >= 1
    @queued.each do |msg|
      Log.newmessage(msg)
    end
    @queued.clear
  end

  # Flush the logging queue.  If there are no destinations available,
  #  adds in a console logger before flushing the queue.
  # This is mainly intended to be used as a last-resort attempt
  #  to ensure that logging messages are not thrown away before
  #  the program is about to exit--most likely in a horrific
  #  error scenario.
  # @return nil
  def Log.force_flushqueue()
    if (@destinations.empty? and !(@queued.empty?))
      newdestination(:console)
    end
    flushqueue
  end

  def Log.sendlevel?(level)
    @levels.index(level) >= @loglevel
  end

  # Reopen all of our logs.
  def Log.reopen
    Puppet.notice _("Reopening log files")
    types = @destinations.keys
    @destinations.each { |type, dest|
      dest.close if dest.respond_to?(:close)
    }
    @destinations.clear
    # We need to make sure we always end up with some kind of destination
    begin
      types.each { |type|
        Log.newdestination(type)
      }
    rescue => detail
      if @destinations.empty?
        Log.setup_default
        Puppet.err detail.to_s
      end
    end
  end

  def self.setup_default
    Log.newdestination(
      (Puppet.features.syslog?   ? :syslog   :
      (Puppet.features.eventlog? ? :eventlog : Puppet[:puppetdlog])))
  end

  # Is the passed level a valid log level?
  def self.validlevel?(level)
    @levels.include?(level)
  end

  def self.from_data_hash(data)
    obj = allocate
    obj.initialize_from_hash(data)
    obj
  end

  # Log output using scope and level
  #
  # @param [Puppet::Parser::Scope] scope
  # @param [Symbol] level log level
  # @param [Array<Object>] vals the values to log (will be converted to string and joined with space)
  #
  def self.log_func(scope, level, vals)
    # NOTE: 3x, does this: vals.join(" ")
    # New implementation uses the evaluator to get proper formatting per type
    vals = vals.map { |v| Puppet::Pops::Evaluator::EvaluatorImpl.new.string(v, scope) }

    # Bypass Puppet.<level> call since it picks up source from "self" which is not applicable in the 4x
    # Function API.
    # TODO: When a function can obtain the file, line, pos of the call merge those in (3x supports
    #       options :file, :line. (These were never output when calling the 3x logging functions since
    #       3x scope does not know about the calling location at that detailed level, nor do they
    #       appear in a report to stdout/error when included). Now, the output simply uses scope (like 3x)
    #       as this is good enough, but does not reflect the true call-stack, but is a rough estimate
    #       of where the logging call originates from).
    #
    Puppet::Util::Log.create({:level => level, :source => scope, :message => vals.join(" ")})
    nil
  end


  attr_accessor :time, :remote, :file, :line, :pos, :source, :issue_code, :environment, :node, :backtrace
  attr_reader :level, :message

  def initialize(args)
    self.level = args[:level]
    self.message = args[:message]
    self.source = args[:source] || "Puppet"

    @time = Time.now

    tags = args[:tags]
    if tags
      tags.each { |t| self.tag(t) }
    end

    # Don't add these unless defined (preserve 3.x API as much as possible)
    [:file, :line, :pos, :issue_code, :environment, :node, :backtrace].each do |attr|
      value = args[attr]
      next unless value
      send(attr.to_s + '=', value)
    end

    Log.newmessage(self)
  end

  def initialize_from_hash(data)
    @level = data['level'].intern
    @message = data['message']
    @source = data['source']
    @tags = Puppet::Util::TagSet.new(data['tags'])
    @time = data['time']
    if @time.is_a? String
      @time = Time.parse(@time)
    end
    # Don't add these unless defined (preserve 3.x API as much as possible)
    %w(file line pos issue_code environment node backtrace).each do |name|
      value = data[name]
      next unless value
      send(name + '=', value)
    end
  end

  def to_hash
    self.to_data_hash
  end

  def to_data_hash
    {
      'level' => @level.to_s,
      'message' => to_s,
      'source' => @source,
      'tags' => @tags.to_a,
      'time' => @time.iso8601(9),
      'file' => @file,
      'line' => @line,
    }
  end

  def to_structured_hash
    hash = {
      'level' => @level,
      'message' => @message,
      'source' => @source,
      'tags' => @tags.to_a,
      'time' => @time.iso8601(9),
    }
    %w(file line pos issue_code environment node backtrace).each do |name|
      attr_name = "@#{name}"
      hash[name] = instance_variable_get(attr_name) if instance_variable_defined?(attr_name)
    end
    hash
  end

  def message=(msg)
    #TRANSLATORS 'Puppet::Util::Log' refers to a Puppet source code class
    raise ArgumentError, _("Puppet::Util::Log requires a message") unless msg
    @message = msg.to_s
  end

  def level=(level)
    #TRANSLATORS 'Puppet::Util::Log' refers to a Puppet source code class
    raise ArgumentError, _("Puppet::Util::Log requires a log level") unless level
    #TRANSLATORS 'Puppet::Util::Log' refers to a Puppet source code class
    raise ArgumentError, _("Puppet::Util::Log requires a symbol or string") unless level.respond_to? "to_sym"
    @level = level.to_sym
    raise ArgumentError, _("Invalid log level %{level}") % { level: @level } unless self.class.validlevel?(@level)

    # Tag myself with my log level
    tag(level)
  end

  # If they pass a source in to us, we make sure it is a string, and
  # we retrieve any tags we can.
  def source=(source)
    if defined?(Puppet::Type) && source.is_a?(Puppet::Type)
      @source = source.path
      merge_tags_from(source)
      self.file = source.file
      self.line = source.line
    else
      @source = source.to_s
    end
  end

  def to_report
    "#{time} #{source} (#{level}): #{self}"
  end

  def to_s
    msg = message

    # Issue based messages do not have details in the message. It
    # must be appended here
    unless issue_code.nil?
      msg = _("Could not parse for environment %{environment}: %{msg}") % { environment: environment, msg: msg } unless environment.nil?
      msg += Puppet::Util::Errors.error_location_with_space(file, line, pos)
      msg = _("%{msg} on node %{node}") % { msg: msg, node: node } unless node.nil?
      if @backtrace.is_a?(Array)
        msg += "\n"
        msg += @backtrace.join("\n")
      end
    end
    msg
  end

end

# This is for backward compatibility from when we changed the constant to
# Puppet::Util::Log because the reports include the constant name. It was
# considered for removal but left in due to risk of breakage (PUP-7502).
Puppet::Log = Puppet::Util::Log