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
|
# frozen_string_literal: true
module Lumberjack
# A structured representation of a single log entry containing the message,
# metadata, and contextual information. LogEntry objects are immutable data
# structures that capture all relevant information about a logging event,
# including timing, severity, source identification, and custom attributes.
#
# This class serves as the fundamental data structure passed between loggers,
# formatters, and output devices throughout the Lumberjack logging pipeline.
# Each entry maintains consistent structure while supporting flexible attribute
# attachment for contextual logging scenarios.
class LogEntry
# @!attribute [rw] time
# @return [Time] The timestamp when the log entry was created
# @!attribute [rw] message
# @return [String] The primary log message content
# @!attribute [rw] severity
# @return [Integer] The numeric severity level of the log entry
# @!attribute [rw] progname
# @return [String] The name of the program or component that generated the entry
# @!attribute [rw] pid
# @return [Integer] The process ID of the logging process
# @!attribute [rw] attributes
# @return [Hash<String, Object>] Custom attributes associated with the log entry
attr_accessor :time, :message, :severity, :progname, :pid, :attributes
TIME_FORMAT = "%Y-%m-%dT%H:%M:%S.%3N"
# Create a new log entry with the specified components. The entry captures
# all relevant information about a logging event in a structured format.
#
# @param time [Time] The timestamp when the log entry was created
# @param severity [Integer, String, Symbol] The severity level, accepts numeric levels or labels
# @param message [String] The primary log message content
# @param progname [String, nil] The name of the program or component generating the entry
# @param pid [Integer] The process ID of the logging process
# @param attributes [Hash<String, Object>, nil] Custom attributes to associate with the entry
def initialize(time, severity, message, progname, pid, attributes)
@time = time
@severity = (severity.is_a?(Integer) ? severity : Severity.label_to_level(severity))
@message = message
@progname = progname
@pid = pid
@attributes = flatten_attributes(attributes) if attributes.is_a?(Hash)
end
# Get the human-readable severity label corresponding to the numeric severity level.
#
# @return [String] The severity label (DEBUG, INFO, WARN, ERROR, FATAL, or UNKNOWN)
def severity_label
severity_data.label
end
# Get the data corresponding to the severity. This include variations on the severity label.
def severity_data
Severity.data(severity)
end
# Generate a formatted string representation of the log entry suitable for
# human consumption. Includes timestamp, severity, program name, process ID,
# attributes, and the main message.
#
# @return [String] A formatted string representation of the complete log entry
def to_s
msg = +"[#{time.strftime(TIME_FORMAT)} #{severity_label} #{progname}(#{pid})] #{message}"
attributes&.each do |key, value|
msg << " [#{key}:#{value}]"
end
msg
end
# Return a string representation suitable for debugging and inspection.
#
# @return [String] The same as {#to_s}
def inspect
to_s
end
# Compare this log entry with another for equality. Two log entries are
# considered equal if all their components match exactly.
#
# @param other [Object] The object to compare against
# @return [Boolean] True if the entries are identical, false otherwise
def ==(other)
return true if equal?(other)
return false unless other.is_a?(LogEntry)
time == other.time &&
severity == other.severity &&
message == other.message &&
progname == other.progname &&
pid == other.pid &&
attributes == other.attributes
end
# Alias for tags to provide backward compatibility with version 1.x API. This method
# will eventually be removed.
#
# @return [Hash, nil] The attributes of the log entry.
# @deprecated Use {#attributes} instead.
def tags
Utils.deprecated("LogEntry#tags", "Lumberjack::LogEntry#tags is deprecated and will be removed in version 2.1; use attributes instead.") do
attributes
end
end
# Access an attribute value by name. Supports both simple and nested attribute
# access using dot notation for hierarchical data structures.
#
# @param name [String, Symbol] The attribute name, supports dot notation for nested access
# @return [Object, nil] The attribute value or nil if the attribute does not exist
def [](name)
return nil if attributes.nil?
AttributesHelper.new(attributes)[name]
end
# Alias method for #[] to provide backward compatibility with version 1.x API. This
# method will eventually be removed.
#
# @return [Hash]
# @deprecated Use {#[]} instead.
def tag(name)
Utils.deprecated("LogEntry#tag", "Lumberjack::LogEntry#tag is deprecated and will be removed in version 2.1; use [] instead.") do
self[name]
end
end
# Expand flat attributes with dot notation into a nested hash structure.
# Attributes containing dots in their names are converted into hierarchical
# nested hashes for structured data representation.
#
# @return [Hash] The attributes expanded into a nested structure
def nested_attributes
Utils.expand_attributes(attributes)
end
# Alias for nested_attributes to provide API compatibility with version 1.x.
# This method will eventually be removed.
#
# @return [Hash]
# @deprecated Use {#nested_attributes} instead.
def nested_tags
Utils.deprecated("LogEntry#nested_tags", "Lumberjack::LogEntry#nested_tags is deprecated and will be removed in version 2.1; use nested_attributes instead.") do
nested_attributes
end
end
# Determine if the log entry contains no meaningful content. An entry is
# considered empty if it has no message content and no attributes.
#
# @return [Boolean] True if the entry is empty, false otherwise
def empty?
(message.nil? || message == "") && (attributes.nil? || attributes.empty?)
end
# Convert the log entry into a hash suitable for JSON serialization. Attributes will be expanded
# into a nested structure (i.e. { "user.id" => 123 } becomes `{ "user" => { "id" => 123 } }).
# Severities will be converted to their string labels.
#
# @return [Hash] The JSON representation of the log entry
def as_json
{
"time" => time,
"severity" => severity_label,
"message" => message,
"progname" => progname,
"pid" => pid,
"attributes" => Utils.expand_attributes(attributes)
}
end
private
# Generate a string representation of all attributes for inclusion in the
# formatted output. Each attribute is formatted as key:value pairs.
#
# @return [String] A formatted string of all attributes
def attributes_to_s
attributes_string = +""
attributes&.each { |name, value| attributes_string << " #{name}:#{value.inspect}" }
attributes_string
end
# Flatten nested attributes and remove empty values.
#
# @param attributes [Hash] The attributes hash to compact
# @return [Hash] The flattened attributes with empty values removed
def flatten_attributes(attributes)
unless attributes.all? { |key, value| key.is_a?(String) && !value.is_a?(Hash) }
attributes = Utils.flatten_attributes(attributes)
end
delete_keys = nil
attributes.each do |key, value|
if value.nil? || value == ""
delete_keys ||= []
delete_keys << key
elsif value.is_a?(Array) && value.empty?
delete_keys ||= []
delete_keys << key
end
end
return attributes if delete_keys.nil?
attributes = attributes.dup
delete_keys&.each { |key| attributes.delete(key) }
attributes
end
end
end
|