File: utils.rb

package info (click to toggle)
ruby-lumberjack 2.0.4-1
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 956 kB
  • sloc: ruby: 7,957; makefile: 2
file content (332 lines) | stat: -rw-r--r-- 13,724 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
# frozen_string_literal: true

require "socket"

module Lumberjack
  # Error raised when a deprecated method is called and the deprecation mode is set to :raise.
  class DeprecationError < StandardError
  end

  # Utils provides utility methods and helper functions used throughout the Lumberjack logging framework.
  module Utils
    UNDEFINED = Object.new.freeze
    private_constant :UNDEFINED

    NON_SLUGGABLE_PATTERN = /[^A-Za-z0-9_.-]+/.freeze
    private_constant :NON_SLUGGABLE_PATTERN

    @deprecations = nil
    @deprecations_lock = nil
    @hostname = UNDEFINED

    class << self
      # Print warning when deprecated methods are called the first time. This can be disabled
      # by setting +Lumberjack.deprecation_mode+ to +:silent+.
      #
      # In order to cut down on noise, each deprecated method will only print a warning once per process.
      # You can change this by setting +Lumberjack.deprecation_mode+ to +:verbose+.
      #
      # @param method [String, Symbol] The name of the deprecated method.
      # @param message [String] The deprecation message explaining what to use instead.
      # @yield The block containing the deprecated functionality to execute.
      # @return [Object] The result of the yielded block.
      #
      # @example
      #   def old_method
      #     Utils.deprecated(:old_method, "Use new_method instead.") do
      #       # deprecated implementation
      #     end
      #   end
      def deprecated(method, message)
        if Lumberjack.deprecation_mode != :silent && !@deprecations&.include?(method)
          @deprecations_lock ||= Mutex.new
          @deprecations_lock.synchronize do
            @deprecations ||= {}
            unless @deprecations.include?(method)
              trace = ($VERBOSE && Lumberjack.deprecation_mode != :raise) ? caller[3..] : caller[3, 1]
              if trace.first.start_with?(__dir__) && !$VERBOSE
                non_lumberjack_caller = caller[4..].detect { |line| !line.start_with?(__dir__) }
                trace = [non_lumberjack_caller] if non_lumberjack_caller
              end
              message = "DEPRECATION WARNING: #{message} Called from #{trace.join(Lumberjack::LINE_SEPARATOR)}"

              if Lumberjack.deprecation_mode == :raise
                raise DeprecationError, message
              end

              unless Lumberjack.deprecation_mode == :verbose
                @deprecations[method] = true
              end

              warn(message)
            end
          end
        end

        yield if block_given?
      end

      # Helper method for tests to silence deprecation warnings within a block. You should
      # not use this in production code since it will silence all deprecation warnings
      # globally across all threads.
      #
      # @param mode [Symbol, String] The deprecation mode to set within the block. Valid values are
      #   :normal, :verbose, :silent, and :raise.
      # @yield The block in which to silence deprecation warnings.
      # @return [Object] The result of the yielded block.
      def with_deprecation_mode(mode)
        save_mode = Lumberjack.deprecation_mode
        begin
          Lumberjack.deprecation_mode = mode
          yield
        ensure
          Lumberjack.deprecation_mode = save_mode
        end
      end

      # Get the hostname of the machine. The returned value will be in UTF-8 encoding.
      # The hostname is cached after the first call for performance.
      #
      # @return [String] The hostname of the machine in UTF-8 encoding.
      def hostname
        if @hostname.equal?(UNDEFINED)
          @hostname = force_utf8(Socket.gethostname)
        end
        @hostname
      end

      # Get the current line of code that calls this method. This is useful for debugging
      # purposes to record the exact location in your code that generated a log entry.
      #
      # @param root_path [String, Pathname, nil] An optional root path to strip from the file path.
      # @return [String] A string representation of the caller location (file:line:method).
      #
      # @example Adding source location to log entries
      #   logger.info("Something happened", source: Lumberjack::Utils.current_line)
      #   # Logs: "Something happened" with source: "/path/to/file.rb:123:in `method_name'"
      def current_line(root_path = nil)
        location = caller_locations(1, 1)[0]
        path = location.path
        if root_path
          root_path = root_path.to_s
          root_path = "#{root_path}#{File::SEPARATOR}" unless root_path.end_with?(File::SEPARATOR)
          path = path.delete_prefix(root_path)
        end
        "#{path}:#{location.lineno}:in `#{location.label}'"
      end

      # Set the hostname to a specific value. This overrides the system hostname.
      # Useful for testing or when you want to use a specific identifier.
      #
      # @param hostname [String] The hostname to use.
      # @return [void]
      def hostname=(hostname)
        @hostname = force_utf8(hostname)
      end

      # Generate a global process identifier that includes the hostname and process ID.
      # This creates a unique identifier that can distinguish processes across different machines.
      #
      # @return [String] The global process ID in the format "hostname-pid".
      #
      # @example
      #   Lumberjack::Utils.global_pid
      #   # => "server1-12345"
      def global_pid(pid = Process.pid)
        if hostname
          "#{hostname}-#{pid}"
        else
          pid.to_s
        end
      end

      # Generate a global thread identifier that includes the global process ID and thread name.
      # This creates a unique identifier for threads across processes and machines.
      #
      # @return [String] The global thread ID in the format "hostname-pid-threadname".
      #
      # @example
      #   Lumberjack::Utils.global_thread_id
      #   # => "server1-12345-main" or "server1-12345-worker-1"
      def global_thread_id
        "#{global_pid}-#{thread_name}"
      end

      # Get a safe name for a thread. Uses the thread's assigned name if available,
      # otherwise generates a unique identifier based on the thread's object ID.
      # Non-alphanumeric characters (except underscores, dashes, and periods) are replaced
      # with dashes to create URL-safe identifiers.
      #
      # @param thread [Thread] The thread to get the name for. Defaults to the current thread.
      # @return [String] A safe string identifier for the thread.
      #
      # @example
      #   Thread.current.name = "worker-thread"
      #   Lumberjack::Utils.thread_name  # => "worker-thread"
      #
      #   # For unnamed threads
      #   Lumberjack::Utils.thread_name  # => "2c001a80c" (based on object_id)
      def thread_name(thread = Thread.current)
        thread.name ? slugify(thread.name) : thread.object_id.to_s(36)
      end

      # Force encode a string to UTF-8, handling invalid byte sequences gracefully.
      # Any invalid or undefined byte sequences will be replaced with an empty string,
      # ensuring the result is always valid UTF-8.
      #
      # @param str [String, nil] The string to encode. Returns nil if input is nil.
      # @return [String, nil] The UTF-8 encoded string, or nil if input was nil.
      #
      # @example
      #   # Handles strings with invalid encoding
      #   bad_string = "Hello\xff\xfeWorld".force_encoding("ASCII-8BIT")
      #   Lumberjack::Utils.force_utf8(bad_string)  # => "HelloWorld"
      def force_utf8(str)
        return nil if str.nil?

        str.dup.force_encoding("ASCII-8BIT").encode("UTF-8", invalid: :replace, undef: :replace, replace: "")
      end

      # Flatten a nested attribute hash into a single-level hash using dot notation for nested keys.
      # This is useful for converting structured data into a flat format suitable for logging systems
      # that don't support nested structures.
      #
      # @param attr_hash [Hash] The hash to flatten. Non-hash values are ignored.
      # @return [Hash<String, Object>] A flattened hash with dot-notation keys.
      #
      # @example Basic flattening
      #   hash = {user: {id: 123, profile: {name: "Alice"}}, action: "login"}
      #   Lumberjack::Utils.flatten_attributes(hash)
      #   # => {"user.id" => 123, "user.profile.name" => "Alice", "action" => "login"}
      #
      # @example With mixed types
      #   hash = {config: {db: {host: "localhost", port: 5432}}, debug: true}
      #   Lumberjack::Utils.flatten_attributes(hash)
      #   # => {"config.db.host" => "localhost", "config.db.port" => 5432, "debug" => true}
      def flatten_attributes(attr_hash)
        return {} unless attr_hash.is_a?(Hash)

        flatten_hash_recursive(attr_hash)
      end

      # Alias for {.flatten_attributes} to provide compatibility with the 1.x API.
      # This method will eventually be removed in a future version.
      #
      # @param tag_hash [Hash] The hash to flatten.
      # @return [Hash<String, Object>] The flattened hash.
      # @deprecated Use {.flatten_attributes} instead.
      def flatten_tags(tag_hash)
        Utils.deprecated("Lumberjack::Utils.flatten_tags", "Lumberjack::Utils.flatten_tags is deprecated and will be removed in version 2.1; use flatten_attributes instead.") do
          flatten_attributes(tag_hash)
        end
      end

      # Expand a hash containing dot notation keys into a nested hash structure.
      # This is the inverse operation of {.flatten_attributes} and is useful for converting
      # flat attribute structures back into nested hashes.
      #
      # @param attributes [Hash] The hash with dot notation keys to expand. Non-hash values are ignored.
      # @return [Hash] A nested hash with dot notation keys expanded into nested structures.
      #
      # @example Basic expansion
      #   flat = {"user.id" => 123, "user.name" => "Alice", "action" => "login"}
      #   Lumberjack::Utils.expand_attributes(flat)
      #   # => {"user" => {"id" => 123, "name" => "Alice"}, "action" => "login"}
      #
      # @example Deep nesting
      #   flat = {"app.db.host" => "localhost", "app.db.port" => 5432, "app.debug" => true}
      #   Lumberjack::Utils.expand_attributes(flat)
      #   # => {"app" => {"db" => {"host" => "localhost", "port" => 5432}, "debug" => true}}
      #
      # @example Mixed with existing nested structures
      #   mixed = {"user.id" => 123, "settings" => {"theme" => "dark"}}
      #   Lumberjack::Utils.expand_attributes(mixed)
      #   # => {"user" => {"id" => 123}, "settings" => {"theme" => "dark"}}
      def expand_attributes(attributes)
        return {} unless attributes.is_a?(Hash)

        expand_dot_notation_hash(attributes)
      end

      # Alias for {.expand_attributes} to provide compatibility with the 1.x API.
      # This method will eventually be removed in a future version.
      #
      # @param tags [Hash] The hash to expand.
      # @return [Hash] The expanded hash.
      # @deprecated Use {.expand_attributes} instead.
      def expand_tags(tags)
        Utils.deprecated("Lumberjack::Utils.expand_tags", "Lumberjack::Utils.expand_tags is deprecated and will be removed in version 2.1; use expand_attributes instead.") do
          expand_attributes(tags)
        end
      end

      private

      # Recursively flatten a hash, building dot notation keys for nested structures.
      #
      # @param hash [Hash] The hash to flatten.
      # @param prefix [String, nil] The current key prefix for nested structures.
      # @return [Hash<String, Object>] The flattened hash.
      # @api private
      def flatten_hash_recursive(hash, prefix = nil)
        hash.each_with_object({}) do |(key, value), result|
          full_key = prefix ? "#{prefix}.#{key}" : key.to_s
          if value.is_a?(Hash)
            result.merge!(flatten_hash_recursive(value, full_key))
          else
            result[full_key] = value
          end
        end
      end

      # Convert a string to a URL-safe slug by replacing non-alphanumeric characters
      # (except underscores, dashes, and periods) with dashes, and removing leading/trailing dashes.
      #
      # @param str [String, nil] The string to slugify.
      # @return [String, nil] The slugified string, or nil if input was nil.
      # @api private
      def slugify(str)
        return nil if str.nil?

        str = str.gsub(NON_SLUGGABLE_PATTERN, "-")
        str.delete_prefix!("-")
        str.chomp!("-")
        str
      end

      # Recursively expand dot notation keys in a hash into nested structures.
      #
      # @param hash [Hash] The hash containing dot notation keys to expand.
      # @param expanded [Hash] The target hash to store expanded results.
      # @return [Hash] The expanded hash with nested structures.
      # @api private
      def expand_dot_notation_hash(hash, expanded = {})
        return hash unless hash.is_a?(Hash)

        hash.each do |key, value|
          key = key.to_s
          if key.include?(".")
            main_key, sub_key = key.split(".", 2)
            main_key_hash = expanded[main_key]
            unless main_key_hash.is_a?(Hash)
              main_key_hash = {}
              expanded[main_key] = main_key_hash
            end
            expand_dot_notation_hash({sub_key => value}, main_key_hash)
          elsif value.is_a?(Hash)
            key_hash = expanded[key]
            unless key_hash.is_a?(Hash)
              key_hash = {}
              expanded[key] = key_hash
            end
            expand_dot_notation_hash(value, key_hash)
          else
            expanded[key] = value
          end
        end

        expanded
      end
    end
  end
end