lumberjack 1.4.2 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. checksums.yaml +4 -4
  2. data/ARCHITECTURE.md +524 -176
  3. data/CHANGELOG.md +89 -0
  4. data/README.md +604 -211
  5. data/UPGRADE_GUIDE.md +80 -0
  6. data/VERSION +1 -1
  7. data/lib/lumberjack/attribute_formatter.rb +451 -0
  8. data/lib/lumberjack/attributes_helper.rb +100 -0
  9. data/lib/lumberjack/context.rb +120 -23
  10. data/lib/lumberjack/context_logger.rb +620 -0
  11. data/lib/lumberjack/device/buffer.rb +209 -0
  12. data/lib/lumberjack/device/date_rolling_log_file.rb +10 -62
  13. data/lib/lumberjack/device/log_file.rb +76 -29
  14. data/lib/lumberjack/device/logger_wrapper.rb +137 -0
  15. data/lib/lumberjack/device/multi.rb +92 -30
  16. data/lib/lumberjack/device/null.rb +26 -8
  17. data/lib/lumberjack/device/size_rolling_log_file.rb +13 -54
  18. data/lib/lumberjack/device/test.rb +337 -0
  19. data/lib/lumberjack/device/writer.rb +184 -176
  20. data/lib/lumberjack/device.rb +134 -15
  21. data/lib/lumberjack/device_registry.rb +90 -0
  22. data/lib/lumberjack/entry_formatter.rb +357 -0
  23. data/lib/lumberjack/fiber_locals.rb +55 -0
  24. data/lib/lumberjack/forked_logger.rb +143 -0
  25. data/lib/lumberjack/formatter/date_time_formatter.rb +14 -3
  26. data/lib/lumberjack/formatter/exception_formatter.rb +12 -2
  27. data/lib/lumberjack/formatter/id_formatter.rb +13 -1
  28. data/lib/lumberjack/formatter/inspect_formatter.rb +14 -1
  29. data/lib/lumberjack/formatter/multiply_formatter.rb +10 -0
  30. data/lib/lumberjack/formatter/object_formatter.rb +13 -1
  31. data/lib/lumberjack/formatter/pretty_print_formatter.rb +15 -2
  32. data/lib/lumberjack/formatter/redact_formatter.rb +18 -3
  33. data/lib/lumberjack/formatter/round_formatter.rb +12 -0
  34. data/lib/lumberjack/formatter/string_formatter.rb +9 -1
  35. data/lib/lumberjack/formatter/strip_formatter.rb +13 -1
  36. data/lib/lumberjack/formatter/structured_formatter.rb +18 -2
  37. data/lib/lumberjack/formatter/tagged_message.rb +10 -32
  38. data/lib/lumberjack/formatter/tags_formatter.rb +32 -0
  39. data/lib/lumberjack/formatter/truncate_formatter.rb +8 -1
  40. data/lib/lumberjack/formatter.rb +271 -141
  41. data/lib/lumberjack/formatter_registry.rb +84 -0
  42. data/lib/lumberjack/io_compatibility.rb +133 -0
  43. data/lib/lumberjack/local_log_template.rb +209 -0
  44. data/lib/lumberjack/log_entry.rb +154 -79
  45. data/lib/lumberjack/log_entry_matcher/score.rb +276 -0
  46. data/lib/lumberjack/log_entry_matcher.rb +126 -0
  47. data/lib/lumberjack/logger.rb +328 -556
  48. data/lib/lumberjack/message_attributes.rb +38 -0
  49. data/lib/lumberjack/rack/context.rb +66 -15
  50. data/lib/lumberjack/rack.rb +0 -2
  51. data/lib/lumberjack/remap_attribute.rb +24 -0
  52. data/lib/lumberjack/severity.rb +52 -15
  53. data/lib/lumberjack/tag_context.rb +8 -71
  54. data/lib/lumberjack/tag_formatter.rb +22 -188
  55. data/lib/lumberjack/tags.rb +15 -21
  56. data/lib/lumberjack/template.rb +252 -62
  57. data/lib/lumberjack/template_registry.rb +60 -0
  58. data/lib/lumberjack/utils.rb +198 -48
  59. data/lib/lumberjack.rb +167 -59
  60. data/lumberjack.gemspec +4 -2
  61. metadata +41 -15
  62. data/lib/lumberjack/device/rolling_log_file.rb +0 -145
  63. data/lib/lumberjack/rack/request_id.rb +0 -31
  64. data/lib/lumberjack/rack/unit_of_work.rb +0 -21
  65. data/lib/lumberjack/tagged_logger_support.rb +0 -81
  66. data/lib/lumberjack/tagged_logging.rb +0 -29
@@ -0,0 +1,133 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lumberjack
4
+ # IOCompatibility provides methods that allow a logger to be used as an IO-like stream.
5
+ # This enables loggers to be used anywhere an IO object is expected, such as for
6
+ # redirecting standard output/error or integrating with libraries that expect stream objects.
7
+ #
8
+ # When used as a stream, all written values are logged with UNKNOWN severity and include
9
+ # timestamps and other standard log entry metadata. This is particularly useful for:
10
+ # - Capturing output from external libraries or subprocesses
11
+ # - Redirecting STDOUT/STDERR to logs
12
+ # - Providing a logging destination that conforms to IO interface expectations
13
+ #
14
+ # The module implements the essential IO methods like write, puts, print, printf, flush,
15
+ # and close to provide broad compatibility with Ruby's IO ecosystem.
16
+ #
17
+ # @example Basic stream usage
18
+ # logger = Lumberjack::Logger.new(STDOUT)
19
+ # logger.puts("Hello, world!") # Logs with UNKNOWN severity
20
+ # logger.write("Direct write") # Also logs with UNKNOWN severity
21
+ #
22
+ # @example Setting the log entry severity
23
+ # logger = Lumberjack::Logger.new(STDOUT)
24
+ # logger.default_severity = :info
25
+ # logger.puts("This is an info message") # Logs with INFO severity
26
+ #
27
+ # @example Using as STDOUT replacement
28
+ # logger = Lumberjack::Logger.new("/var/log/app.log")
29
+ # $stdout = logger # Redirect all puts/print calls to the logger
30
+ # puts "This goes to the log file"
31
+ module IOCompatibility
32
+ # Write a value to the log as a log entry. The value will be recorded with UNKNOWN severity,
33
+ # ensuring it always appears in the log regardless of the current log level.
34
+ #
35
+ # @param value [Object] The message to write. Will be converted to a string for logging.
36
+ # @return [Integer] Returns 1 if a log entry was written, or 0 if the value was nil or empty.
37
+ def write(value)
38
+ return 0 if value.nil? || value == ""
39
+
40
+ self << value
41
+ 1
42
+ end
43
+
44
+ # Write multiple values to the log, each as a separate log entry with UNKNOWN severity.
45
+ # This method mimics the behavior of IO#puts by writing each argument on a separate line.
46
+ #
47
+ # @param args [Array<Object>] The messages to write. Each will be converted to a string.
48
+ # @return [nil]
49
+ def puts(*args)
50
+ args.each do |arg|
51
+ write(arg)
52
+ end
53
+ nil
54
+ end
55
+
56
+ # Concatentate strings into a single log entry. This mimics IO#print behavior
57
+ # by writing arguments without separators. If no arguments are given, writes the
58
+ # value of the global $_ variable.
59
+ #
60
+ # @param args [Array<Object>] The messages to write. If empty, uses $_ (last input record).
61
+ # @return [nil]
62
+ #
63
+ # @example
64
+ # logger.print("Hello", " ", "World") # Single log entry: "Hello World"
65
+ def print(*args)
66
+ if args.empty?
67
+ write($_)
68
+ else
69
+ write(args.join(""))
70
+ end
71
+ nil
72
+ end
73
+
74
+ # Write a formatted string to the log using sprintf-style formatting. The formatted
75
+ # result is logged as a single entry with UNKNOWN severity.
76
+ #
77
+ # @param format [String] The format string (printf-style format specifiers).
78
+ # @param args [Array<Object>] The values to substitute into the format string.
79
+ # @return [nil]
80
+ #
81
+ # @example
82
+ # logger.printf("User %s logged in at %s", "alice", Time.now)
83
+ # # Logs: "User alice logged in at 2025-08-21 10:30:00 UTC"
84
+ def printf(format, *args)
85
+ write(format % args)
86
+ nil
87
+ end
88
+
89
+ # Flush any buffered output. This method is provided for IO compatibility but
90
+ # is a no-op since log entries are typically written immediately to the underlying device.
91
+ # The actual flushing behavior depends on the logging device being used.
92
+ #
93
+ # @return [nil]
94
+ def flush
95
+ end
96
+
97
+ # Close the stream. This method is provided for IO compatibility but is a no-op.
98
+ # To actually close a logger, call close on the logger object itself, which will
99
+ # close the underlying logging device.
100
+ #
101
+ # @return [nil]
102
+ def close
103
+ end
104
+
105
+ # Check if the stream is closed. Always returns false since loggers using this
106
+ # module don't maintain a closed state through this interface.
107
+ #
108
+ # @return [Boolean] Always returns false.
109
+ def closed?
110
+ false
111
+ end
112
+
113
+ # Check if the stream is connected to a terminal (TTY). Always returns false
114
+ # since loggers are not terminal devices, even when they write to STDOUT/STDERR.
115
+ # This method is required for complete IO compatibility.
116
+ #
117
+ # @return [Boolean] Always returns false.
118
+ # @api private
119
+ def tty?
120
+ false
121
+ end
122
+
123
+ # Set the encoding for the stream. This method is provided for IO compatibility
124
+ # but is a no-op since loggers handle encoding internally through their devices
125
+ # and formatters.
126
+ #
127
+ # @param _encoding [String, Encoding] The encoding to set (ignored).
128
+ # @return [nil]
129
+ # @api private
130
+ def set_encoding(_encoding)
131
+ end
132
+ end
133
+ end
@@ -0,0 +1,209 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lumberjack
4
+ # This is a log template designed for local environments. It provides a simple,
5
+ # human-readable format that includes key information about log entries while
6
+ # omitting extraneous details. The template can be configured to include or
7
+ # exclude certain components such as the times, process ID, program name,
8
+ # and attributes.
9
+ #
10
+ # It is registered with the TemplateRegistry as :local.
11
+ #
12
+ # @see Template
13
+ class LocalLogTemplate
14
+ TemplateRegistry.add(:local, self)
15
+
16
+ # Create a new LocalLogTemplate instance.
17
+ #
18
+ # @param options [Hash] Options for configuring the template.
19
+ # @option options [Boolean, Array<String>, nil] :exclude_attributes If true, all attributes are excluded.
20
+ # If an array of strings is provided, those attributes (and their sub-attributes) are excluded.
21
+ # Defaults to nil (include all attributes).
22
+ # @option options [Boolean] :exclude_progname If true, the progname is excluded. Defaults to false.
23
+ # @option options [Boolean] :exclude_pid If true, the process ID is excluded. Defaults to true.
24
+ # @option options [Boolean] :exclude_time If true, the time is excluded. Defaults to true.
25
+ # @option options [Boolean] :colorize If true, colorize the output based on severity. Defaults to false.
26
+ # @option options [Symbol, Formatter, #call] :exception_formatter The formatter to use for exceptions in messages.
27
+ # Can be a symbol registered in the FormatterRegistry, a Formatter instance, or any object that responds to #call.
28
+ # Defaults to nil (use default exception formatting). If the logger does not have an exception formatter
29
+ # configured, then the device will use this to format exceptions.
30
+ # @option options [String, Symbol] :severity_format The optional format for severity labels (padded, char, emoji).
31
+ def initialize(options = {})
32
+ self.exclude_progname = options.fetch(:exclude_progname, false)
33
+ self.exclude_pid = options.fetch(:exclude_pid, true)
34
+ self.exclude_time = options.fetch(:exclude_time, true)
35
+ self.exclude_attributes = options.fetch(:exclude_attributes, nil)
36
+ self.colorize = options.fetch(:colorize, false)
37
+ self.severity_format = options.fetch(:severity_format, nil)
38
+ self.exception_formatter = options.fetch(:exception_formatter, :exception)
39
+ end
40
+
41
+ # Format a log entry according to the template.
42
+ #
43
+ # @param entry [LogEntry] The log entry to format.
44
+ # @return [String] The formatted log entry.
45
+ def call(entry)
46
+ message = entry.message
47
+ if message.is_a?(Exception) && exception_formatter
48
+ message = exception_formatter.call(message)
49
+ end
50
+
51
+ formatted = +""
52
+ formatted << entry.time.strftime("%Y-%m-%d %H:%M:%S.%6N ") unless exclude_time?
53
+ formatted << "#{severity_label(entry)} #{message}"
54
+ formatted << "#{Lumberjack::LINE_SEPARATOR} progname: #{entry.progname}" if entry.progname.to_s != "" && !exclude_progname?
55
+ formatted << "#{Lumberjack::LINE_SEPARATOR} pid: #{entry.pid}" unless exclude_pid?
56
+
57
+ if entry.attributes && !entry.attributes.empty? && !exclude_attributes?
58
+ Lumberjack::Utils.flatten_attributes(entry.attributes).to_a.sort_by(&:first).each do |name, value|
59
+ next if @attribute_filter.any? do |filter_name|
60
+ if name.start_with?(filter_name)
61
+ next_char = name[filter_name.length]
62
+ next_char.nil? || next_char == "."
63
+ end
64
+ end
65
+
66
+ formatted << "#{Lumberjack::LINE_SEPARATOR} #{name}: #{value}"
67
+ end
68
+ end
69
+
70
+ formatted = Template.colorize_entry(formatted, entry) if colorize?
71
+ formatted << Lumberjack::LINE_SEPARATOR
72
+ end
73
+
74
+ # Return true if all attributes are excluded, false otherwise.
75
+ #
76
+ # @return [Boolean]
77
+ def exclude_attributes?
78
+ @exclude_attributes
79
+ end
80
+
81
+ # Return the list of excluded attribute names.
82
+ #
83
+ # @return [Array<String>]
84
+ def excluded_attributes
85
+ @attribute_filter.dup
86
+ end
87
+
88
+ # Set the attributes to exclude. If set to true, all attributes are excluded.
89
+ # If set to an array of strings, those attributes (and their sub-attributes)
90
+ # are excluded. If set to false or nil, no attributes are excluded.
91
+ #
92
+ # @param value [Boolean, Array<String>, nil]
93
+ # @return [void]
94
+ def exclude_attributes=(value)
95
+ @exclude_attributes = false
96
+ @attribute_filter = []
97
+ if value == true
98
+ @exclude_attributes = true
99
+ elsif value
100
+ @attribute_filter = Array(value).map(&:to_s)
101
+ end
102
+ end
103
+
104
+ # Return true if the progname is excluded, false otherwise.
105
+ #
106
+ # @return [Boolean]
107
+ def exclude_progname?
108
+ @exclude_progname
109
+ end
110
+
111
+ # Set whether to exclude the progname.
112
+ #
113
+ # @param value [Boolean]
114
+ # @return [void]
115
+ def exclude_progname=(value)
116
+ @exclude_progname = !!value
117
+ end
118
+
119
+ # Return true if the pid is excluded, false otherwise.
120
+ #
121
+ # @return [Boolean]
122
+ def exclude_pid?
123
+ @exclude_pid
124
+ end
125
+
126
+ # Set whether to exclude the pid.
127
+ #
128
+ # @param value [Boolean]
129
+ # @return [void]
130
+ def exclude_pid=(value)
131
+ @exclude_pid = !!value
132
+ end
133
+
134
+ # Return true if the time is excluded, false otherwise.
135
+ #
136
+ # @return [Boolean]
137
+ def exclude_time?
138
+ @exclude_time
139
+ end
140
+
141
+ # Set whether to exclude the time.
142
+ #
143
+ # @param value [Boolean]
144
+ # @return [void]
145
+ def exclude_time=(value)
146
+ @exclude_time = !!value
147
+ end
148
+
149
+ # Return true if colorization is enabled, false otherwise.
150
+ #
151
+ # @return [Boolean]
152
+ def colorize?
153
+ @colorize
154
+ end
155
+
156
+ # Set whether to enable colorization.
157
+ #
158
+ # @param value [Boolean]
159
+ # @return [void]
160
+ def colorize=(value)
161
+ @colorize = !!value
162
+ end
163
+
164
+ # Set the severity format.
165
+ #
166
+ # @param value [String, Symbol] The severity format (:padded, :char, :emoji, :level, nil).
167
+ # @return [void]
168
+ def severity_format=(value)
169
+ @severity_format = value.to_s
170
+ end
171
+
172
+ # Return the current severity format.
173
+ #
174
+ # @return [String]
175
+ attr_reader :severity_format
176
+
177
+ # Set the exception formatter. Can be a symbol registered in the FormatterRegistry,
178
+ # a Formatter instance, or any object that responds to #call.
179
+ #
180
+ # @param value [Symbol, Formatter, #call] The exception formatter to use.
181
+ # @return [void]
182
+ def exception_formatter=(value)
183
+ @exception_formatter = value.is_a?(Symbol) ? FormatterRegistry.formatter(value) : value
184
+ end
185
+
186
+ # Return the exception formatter.
187
+ #
188
+ # @return [#call, nil]
189
+ attr_reader :exception_formatter
190
+
191
+ private
192
+
193
+ def severity_label(entry)
194
+ severity = entry.severity_data
195
+ case severity_format
196
+ when "padded"
197
+ severity.padded_label
198
+ when "char"
199
+ severity.char
200
+ when "emoji"
201
+ severity.emoji
202
+ when "level"
203
+ severity.level.to_s
204
+ else
205
+ severity.label
206
+ end
207
+ end
208
+ end
209
+ end
@@ -1,139 +1,214 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Lumberjack
4
- # An entry in a log is a data structure that captures the log message as well as
5
- # information about the system that logged the message.
4
+ # A structured representation of a single log entry containing the message,
5
+ # metadata, and contextual information. LogEntry objects are immutable data
6
+ # structures that capture all relevant information about a logging event,
7
+ # including timing, severity, source identification, and custom attributes.
8
+ #
9
+ # This class serves as the fundamental data structure passed between loggers,
10
+ # formatters, and output devices throughout the Lumberjack logging pipeline.
11
+ # Each entry maintains consistent structure while supporting flexible attribute
12
+ # attachment for contextual logging scenarios.
6
13
  class LogEntry
7
- attr_accessor :time, :message, :severity, :progname, :pid, :tags
8
-
9
- TIME_FORMAT = "%Y-%m-%dT%H:%M:%S"
10
-
11
- # @deprecated Will be removed in version 2.0.
12
- UNIT_OF_WORK_ID = "unit_of_work_id"
13
-
14
- # Create a new log entry.
14
+ # @!attribute [rw] time
15
+ # @return [Time] The timestamp when the log entry was created
16
+ # @!attribute [rw] message
17
+ # @return [String] The primary log message content
18
+ # @!attribute [rw] severity
19
+ # @return [Integer] The numeric severity level of the log entry
20
+ # @!attribute [rw] progname
21
+ # @return [String] The name of the program or component that generated the entry
22
+ # @!attribute [rw] pid
23
+ # @return [Integer] The process ID of the logging process
24
+ # @!attribute [rw] attributes
25
+ # @return [Hash<String, Object>] Custom attributes associated with the log entry
26
+ attr_accessor :time, :message, :severity, :progname, :pid, :attributes
27
+
28
+ TIME_FORMAT = "%Y-%m-%dT%H:%M:%S.%3N"
29
+
30
+ # Create a new log entry with the specified components. The entry captures
31
+ # all relevant information about a logging event in a structured format.
15
32
  #
16
- # @param time [Time] The time the log entry was created.
17
- # @param severity [Integer, String] The severity of the log entry.
18
- # @param message [String] The message to log.
19
- # @param progname [String] The name of the program that created the log entry.
20
- # @param pid [Integer] The process id of the program that created the log entry.
21
- # @param tags [Hash<String, Object>] A hash of tags to associate with the log entry.
22
- def initialize(time, severity, message, progname, pid, tags)
33
+ # @param time [Time] The timestamp when the log entry was created
34
+ # @param severity [Integer, String, Symbol] The severity level, accepts numeric levels or labels
35
+ # @param message [String] The primary log message content
36
+ # @param progname [String, nil] The name of the program or component generating the entry
37
+ # @param pid [Integer] The process ID of the logging process
38
+ # @param attributes [Hash<String, Object>, nil] Custom attributes to associate with the entry
39
+ def initialize(time, severity, message, progname, pid, attributes)
23
40
  @time = time
24
41
  @severity = (severity.is_a?(Integer) ? severity : Severity.label_to_level(severity))
25
42
  @message = message
26
43
  @progname = progname
27
44
  @pid = pid
28
- # backward compatibility with 1.0 API where the last argument was the unit of work id
29
- @tags = if tags.is_a?(Hash)
30
- compact_tags(tags)
31
- elsif !tags.nil?
32
- {UNIT_OF_WORK_ID => tags}
33
- end
45
+ @attributes = flatten_attributes(attributes) if attributes.is_a?(Hash)
34
46
  end
35
47
 
48
+ # Get the human-readable severity label corresponding to the numeric severity level.
49
+ #
50
+ # @return [String] The severity label (DEBUG, INFO, WARN, ERROR, FATAL, or UNKNOWN)
36
51
  def severity_label
37
- Severity.level_to_label(severity)
52
+ severity_data.label
53
+ end
54
+
55
+ # Get the data corresponding to the severity. This include variations on the severity label.
56
+ def severity_data
57
+ Severity.data(severity)
38
58
  end
39
59
 
60
+ # Generate a formatted string representation of the log entry suitable for
61
+ # human consumption. Includes timestamp, severity, program name, process ID,
62
+ # attributes, and the main message.
63
+ #
64
+ # @return [String] A formatted string representation of the complete log entry
40
65
  def to_s
41
- "[#{time.strftime(TIME_FORMAT)}.#{(time.usec / 1000.0).round.to_s.rjust(3, "0")} #{severity_label} #{progname}(#{pid})#{tags_to_s}] #{message}"
66
+ msg = +"[#{time.strftime(TIME_FORMAT)} #{severity_label} #{progname}(#{pid})] #{message}"
67
+ attributes&.each do |key, value|
68
+ msg << " [#{key}:#{value}]"
69
+ end
70
+ msg
42
71
  end
43
72
 
73
+ # Return a string representation suitable for debugging and inspection.
74
+ #
75
+ # @return [String] The same as {#to_s}
44
76
  def inspect
45
77
  to_s
46
78
  end
47
79
 
48
- # @deprecated - backward compatibility with 1.0 API. Will be removed in version 2.0.
49
- def unit_of_work_id
50
- Lumberjack::Utils.deprecated("Lumberjack::LogEntry#unit_of_work_id", "Lumberjack::LogEntry#unit_of_work_id will be removed in version 2.0") do
51
- tags[UNIT_OF_WORK_ID] if tags
52
- end
80
+ # Compare this log entry with another for equality. Two log entries are
81
+ # considered equal if all their components match exactly.
82
+ #
83
+ # @param other [Object] The object to compare against
84
+ # @return [Boolean] True if the entries are identical, false otherwise
85
+ def ==(other)
86
+ return true if equal?(other)
87
+ return false unless other.is_a?(LogEntry)
88
+
89
+ time == other.time &&
90
+ severity == other.severity &&
91
+ message == other.message &&
92
+ progname == other.progname &&
93
+ pid == other.pid &&
94
+ attributes == other.attributes
53
95
  end
54
96
 
55
- # @deprecated - backward compatibility with 1.0 API. Will be removed in version 2.0.
56
- def unit_of_work_id=(value)
57
- Lumberjack::Utils.deprecated("Lumberjack::LogEntry#unit_of_work_id=", "Lumberjack::LogEntry#unit_of_work_id= will be removed in version 2.0") do
58
- if tags
59
- tags[UNIT_OF_WORK_ID] = value
60
- else
61
- @tags = {UNIT_OF_WORK_ID => value}
62
- end
97
+ # Alias for tags to provide backward compatibility with version 1.x API. This method
98
+ # will eventually be removed.
99
+ #
100
+ # @return [Hash, nil] The attributes of the log entry.
101
+ # @deprecated Use {#attributes} instead.
102
+ def tags
103
+ Utils.deprecated("LogEntry#tags", "Lumberjack::LogEntry#tags is deprecated and will be removed in version 2.1; use attributes instead.") do
104
+ attributes
63
105
  end
64
106
  end
65
107
 
66
- # Return the tag with the specified name.
108
+ # Access an attribute value by name. Supports both simple and nested attribute
109
+ # access using dot notation for hierarchical data structures.
67
110
  #
68
- # @param name [String, Symbol] The tag name.
69
- # @return [Object, nil] The tag value or nil if the tag does not exist.
70
- def tag(name)
71
- return nil if tags.nil?
111
+ # @param name [String, Symbol] The attribute name, supports dot notation for nested access
112
+ # @return [Object, nil] The attribute value or nil if the attribute does not exist
113
+ def [](name)
114
+ return nil if attributes.nil?
72
115
 
73
- TagContext.new(tags)[name]
116
+ AttributesHelper.new(attributes)[name]
74
117
  end
75
118
 
76
- # Helper method to expand the tags into a nested structure. Tags with dots in the name
77
- # will be expanded into nested hashes.
119
+ # Alias method for #[] to provide backward compatibility with version 1.x API. This
120
+ # method will eventually be removed.
78
121
  #
79
- # @return [Hash] The tags expanded into a nested structure.
122
+ # @return [Hash]
123
+ # @deprecated Use {#[]} instead.
124
+ def tag(name)
125
+ Utils.deprecated("LogEntry#tag", "Lumberjack::LogEntry#tag is deprecated and will be removed in version 2.1; use [] instead.") do
126
+ self[name]
127
+ end
128
+ end
129
+
130
+ # Expand flat attributes with dot notation into a nested hash structure.
131
+ # Attributes containing dots in their names are converted into hierarchical
132
+ # nested hashes for structured data representation.
80
133
  #
81
- # @example
82
- # entry = Lumberjack::LogEntry.new(Time.now, Logger::INFO, "test", "app", 1500, "a.b.c" => 1, "a.b.d" => 2)
83
- # entry.nested_tags # => {"a" => {"b" => {"c" => 1, "d" => 2}}}
134
+ # @return [Hash] The attributes expanded into a nested structure
135
+ def nested_attributes
136
+ Utils.expand_attributes(attributes)
137
+ end
138
+
139
+ # Alias for nested_attributes to provide API compatibility with version 1.x.
140
+ # This method will eventually be removed.
141
+ #
142
+ # @return [Hash]
143
+ # @deprecated Use {#nested_attributes} instead.
84
144
  def nested_tags
85
- Utils.expand_tags(tags)
145
+ Utils.deprecated("LogEntry#nested_tags", "Lumberjack::LogEntry#nested_tags is deprecated and will be removed in version 2.1; use nested_attributes instead.") do
146
+ nested_attributes
147
+ end
86
148
  end
87
149
 
88
- # Return true if the log entry has no message and no tags.
150
+ # Determine if the log entry contains no meaningful content. An entry is
151
+ # considered empty if it has no message content and no attributes.
89
152
  #
90
- # @return [Boolean] True if the log entry is empty, false otherwise.
153
+ # @return [Boolean] True if the entry is empty, false otherwise
91
154
  def empty?
92
- (message.nil? || message == "") && (tags.nil? || tags.empty?)
155
+ (message.nil? || message == "") && (attributes.nil? || attributes.empty?)
156
+ end
157
+
158
+ # Convert the log entry into a hash suitable for JSON serialization. Attributes will be expanded
159
+ # into a nested structure (i.e. { "user.id" => 123 } becomes `{ "user" => { "id" => 123 } }).
160
+ # Severities will be converted to their string labels.
161
+ #
162
+ # @return [Hash] The JSON representation of the log entry
163
+ def as_json
164
+ {
165
+ "time" => time,
166
+ "severity" => severity_label,
167
+ "message" => message,
168
+ "progname" => progname,
169
+ "pid" => pid,
170
+ "attributes" => Utils.expand_attributes(attributes)
171
+ }
93
172
  end
94
173
 
95
174
  private
96
175
 
97
- def tags_to_s
98
- tags_string = +""
99
- tags&.each { |name, value| tags_string << " #{name}:#{value.inspect}" }
100
- tags_string
176
+ # Generate a string representation of all attributes for inclusion in the
177
+ # formatted output. Each attribute is formatted as key:value pairs.
178
+ #
179
+ # @return [String] A formatted string of all attributes
180
+ def attributes_to_s
181
+ attributes_string = +""
182
+ attributes&.each { |name, value| attributes_string << " #{name}:#{value.inspect}" }
183
+ attributes_string
101
184
  end
102
185
 
103
- def compact_tags(tags, seen = nil)
104
- return {} if seen&.include?(tags.object_id)
186
+ # Flatten nested attributes and remove empty values.
187
+ #
188
+ # @param attributes [Hash] The attributes hash to compact
189
+ # @return [Hash] The flattened attributes with empty values removed
190
+ def flatten_attributes(attributes)
191
+ unless attributes.all? { |key, value| key.is_a?(String) && !value.is_a?(Hash) }
192
+ attributes = Utils.flatten_attributes(attributes)
193
+ end
105
194
 
106
195
  delete_keys = nil
107
- compacted_keys = nil
108
-
109
- tags.each do |key, value|
196
+ attributes.each do |key, value|
110
197
  if value.nil? || value == ""
111
198
  delete_keys ||= []
112
199
  delete_keys << key
113
- elsif value.is_a?(Hash)
114
- seen ||= Set.new
115
- seen << tags.object_id
116
- compacted_value = compact_tags(value, seen)
117
- if compacted_value.empty?
118
- delete_keys ||= []
119
- delete_keys << key
120
- elsif !value.equal?(compacted_value)
121
- compacted_keys ||= []
122
- compacted_keys << [key, compacted_value]
123
- end
124
200
  elsif value.is_a?(Array) && value.empty?
125
201
  delete_keys ||= []
126
202
  delete_keys << key
127
203
  end
128
204
  end
129
205
 
130
- return tags if delete_keys.nil? && compacted_keys.nil?
206
+ return attributes if delete_keys.nil?
131
207
 
132
- tags = tags.dup
133
- delete_keys&.each { |key| tags.delete(key) }
134
- compacted_keys&.each { |key, value| tags[key] = value }
208
+ attributes = attributes.dup
209
+ delete_keys&.each { |key| attributes.delete(key) }
135
210
 
136
- tags
211
+ attributes
137
212
  end
138
213
  end
139
214
  end