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
|