makit 0.0.140 → 0.0.141
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.
- checksums.yaml +4 -4
- data/README.md +41 -41
- data/exe/makit +5 -5
- data/lib/makit/apache.rb +28 -28
- data/lib/makit/auto.rb +48 -48
- data/lib/makit/cli/build_commands.rb +500 -500
- data/lib/makit/cli/generators/base_generator.rb +74 -74
- data/lib/makit/cli/generators/dotnet_generator.rb +50 -50
- data/lib/makit/cli/generators/generator_factory.rb +49 -49
- data/lib/makit/cli/generators/node_generator.rb +50 -50
- data/lib/makit/cli/generators/ruby_generator.rb +77 -77
- data/lib/makit/cli/generators/rust_generator.rb +50 -50
- data/lib/makit/cli/generators/templates/dotnet_templates.rb +167 -167
- data/lib/makit/cli/generators/templates/node_templates.rb +161 -161
- data/lib/makit/cli/generators/templates/ruby/gemfile.rb +26 -26
- data/lib/makit/cli/generators/templates/ruby/gemspec.rb +40 -40
- data/lib/makit/cli/generators/templates/ruby/main_lib.rb +33 -33
- data/lib/makit/cli/generators/templates/ruby/rakefile.rb +35 -35
- data/lib/makit/cli/generators/templates/ruby/readme.rb +63 -63
- data/lib/makit/cli/generators/templates/ruby/test.rb +39 -39
- data/lib/makit/cli/generators/templates/ruby/test_helper.rb +29 -29
- data/lib/makit/cli/generators/templates/ruby/version.rb +29 -29
- data/lib/makit/cli/generators/templates/rust_templates.rb +128 -128
- data/lib/makit/cli/main.rb +69 -69
- data/lib/makit/cli/project_commands.rb +868 -868
- data/lib/makit/cli/repository_commands.rb +661 -661
- data/lib/makit/cli/strategy_commands.rb +203 -203
- data/lib/makit/cli/utility_commands.rb +521 -521
- data/lib/makit/commands/factory.rb +359 -359
- data/lib/makit/commands/middleware/base.rb +73 -73
- data/lib/makit/commands/middleware/cache.rb +248 -248
- data/lib/makit/commands/middleware/command_logger.rb +312 -312
- data/lib/makit/commands/middleware/validator.rb +269 -269
- data/lib/makit/commands/request.rb +316 -316
- data/lib/makit/commands/result.rb +323 -323
- data/lib/makit/commands/runner.rb +388 -385
- data/lib/makit/commands/strategies/base.rb +171 -171
- data/lib/makit/commands/strategies/child_process.rb +165 -165
- data/lib/makit/commands/strategies/factory.rb +136 -136
- data/lib/makit/commands/strategies/synchronous.rb +139 -139
- data/lib/makit/commands.rb +50 -50
- data/lib/makit/configuration/dotnet_project.rb +12 -12
- data/lib/makit/configuration/gitlab_helper.rb +58 -58
- data/lib/makit/configuration/project.rb +168 -168
- data/lib/makit/configuration/rakefile_helper.rb +43 -43
- data/lib/makit/configuration/step.rb +34 -34
- data/lib/makit/configuration/timeout.rb +74 -74
- data/lib/makit/configuration.rb +15 -15
- data/lib/makit/content/default_gitignore.rb +7 -7
- data/lib/makit/content/default_gitignore.txt +225 -225
- data/lib/makit/content/default_rakefile.rb +13 -13
- data/lib/makit/content/gem_rakefile.rb +16 -16
- data/lib/makit/context.rb +1 -1
- data/lib/makit/data.rb +49 -49
- data/lib/makit/directories.rb +140 -140
- data/lib/makit/directory.rb +262 -262
- data/lib/makit/docs/files.rb +89 -89
- data/lib/makit/docs/rake.rb +102 -102
- data/lib/makit/dotnet/cli.rb +69 -69
- data/lib/makit/dotnet/project.rb +217 -217
- data/lib/makit/dotnet/solution.rb +38 -38
- data/lib/makit/dotnet/solution_classlib.rb +239 -239
- data/lib/makit/dotnet/solution_console.rb +264 -264
- data/lib/makit/dotnet/solution_maui.rb +354 -354
- data/lib/makit/dotnet/solution_wasm.rb +275 -275
- data/lib/makit/dotnet/solution_wpf.rb +304 -304
- data/lib/makit/dotnet.rb +102 -102
- data/lib/makit/email.rb +90 -90
- data/lib/makit/environment.rb +142 -142
- data/lib/makit/examples/runner.rb +370 -370
- data/lib/makit/exceptions.rb +45 -45
- data/lib/makit/fileinfo.rb +24 -24
- data/lib/makit/files.rb +43 -43
- data/lib/makit/gems.rb +40 -40
- data/lib/makit/git/cli.rb +54 -54
- data/lib/makit/git/repository.rb +90 -90
- data/lib/makit/git.rb +98 -98
- data/lib/makit/gitlab_runner.rb +59 -59
- data/lib/makit/humanize.rb +137 -137
- data/lib/makit/indexer.rb +47 -47
- data/lib/makit/logging/configuration.rb +308 -308
- data/lib/makit/logging/format_registry.rb +84 -84
- data/lib/makit/logging/formatters/base.rb +39 -39
- data/lib/makit/logging/formatters/console_formatter.rb +140 -140
- data/lib/makit/logging/formatters/json_formatter.rb +65 -65
- data/lib/makit/logging/formatters/plain_text_formatter.rb +71 -71
- data/lib/makit/logging/formatters/text_formatter.rb +64 -64
- data/lib/makit/logging/log_request.rb +119 -119
- data/lib/makit/logging/logger.rb +199 -199
- data/lib/makit/logging/sinks/base.rb +91 -91
- data/lib/makit/logging/sinks/console.rb +72 -72
- data/lib/makit/logging/sinks/file_sink.rb +92 -92
- data/lib/makit/logging/sinks/structured.rb +123 -123
- data/lib/makit/logging/sinks/unified_file_sink.rb +296 -296
- data/lib/makit/logging.rb +565 -565
- data/lib/makit/markdown.rb +75 -75
- data/lib/makit/mp/basic_object_mp.rb +17 -17
- data/lib/makit/mp/command_mp.rb +13 -13
- data/lib/makit/mp/command_request.mp.rb +17 -17
- data/lib/makit/mp/project_mp.rb +199 -199
- data/lib/makit/mp/string_mp.rb +199 -191
- data/lib/makit/nuget.rb +74 -74
- data/lib/makit/port.rb +32 -32
- data/lib/makit/process.rb +163 -163
- data/lib/makit/protoc.rb +107 -107
- data/lib/makit/rake/cli.rb +196 -196
- data/lib/makit/rake/trace_controller.rb +173 -173
- data/lib/makit/rake.rb +80 -80
- data/lib/makit/ruby/cli.rb +185 -185
- data/lib/makit/ruby.rb +25 -25
- data/lib/makit/secrets.rb +51 -51
- data/lib/makit/serializer.rb +130 -130
- data/lib/makit/services/builder.rb +186 -186
- data/lib/makit/services/error_handler.rb +226 -226
- data/lib/makit/services/repository_manager.rb +231 -231
- data/lib/makit/services/validator.rb +112 -112
- data/lib/makit/setup/classlib.rb +101 -101
- data/lib/makit/setup/gem.rb +268 -268
- data/lib/makit/setup/razorclasslib.rb +101 -101
- data/lib/makit/setup/runner.rb +54 -54
- data/lib/makit/setup.rb +5 -5
- data/lib/makit/show.rb +110 -110
- data/lib/makit/storage.rb +126 -126
- data/lib/makit/symbols.rb +170 -170
- data/lib/makit/task_info.rb +130 -130
- data/lib/makit/tasks/at_exit.rb +15 -15
- data/lib/makit/tasks/build.rb +22 -22
- data/lib/makit/tasks/clean.rb +13 -13
- data/lib/makit/tasks/configure.rb +10 -10
- data/lib/makit/tasks/format.rb +10 -10
- data/lib/makit/tasks/hook_manager.rb +443 -443
- data/lib/makit/tasks/init.rb +49 -49
- data/lib/makit/tasks/integrate.rb +29 -29
- data/lib/makit/tasks/pull_incoming.rb +13 -13
- data/lib/makit/tasks/setup.rb +13 -13
- data/lib/makit/tasks/sync.rb +17 -17
- data/lib/makit/tasks/tag.rb +16 -16
- data/lib/makit/tasks/task_monkey_patch.rb +81 -81
- data/lib/makit/tasks/test.rb +22 -22
- data/lib/makit/tasks/update.rb +18 -18
- data/lib/makit/tasks.rb +20 -20
- data/lib/makit/test_cache.rb +239 -239
- data/lib/makit/tree.rb +37 -37
- data/lib/makit/v1/makit.v1_pb.rb +35 -35
- data/lib/makit/v1/makit.v1_services_pb.rb +27 -27
- data/lib/makit/version.rb +99 -99
- data/lib/makit/version_util.rb +21 -21
- data/lib/makit/wix.rb +95 -95
- data/lib/makit/yaml.rb +29 -29
- data/lib/makit/zip.rb +17 -17
- data/lib/makit copy.rb +44 -44
- data/lib/makit.rb +42 -42
- metadata +2 -2
@@ -1,296 +1,296 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require_relative "base"
|
4
|
-
require_relative "../format_registry"
|
5
|
-
require "fileutils"
|
6
|
-
|
7
|
-
module Makit
|
8
|
-
module Logging
|
9
|
-
module Sinks
|
10
|
-
# Unified file sink with configurable multiple outputs
|
11
|
-
#
|
12
|
-
# This sink replaces the need for multiple separate sinks by providing
|
13
|
-
# a single, highly configurable sink that can write to multiple files
|
14
|
-
# with different formats, filtering, and options. It supports console
|
15
|
-
# output, file logging, and structured logging through configuration.
|
16
|
-
#
|
17
|
-
# @example Basic usage with console and file output
|
18
|
-
# sink = UnifiedFileSink.new(
|
19
|
-
# configurations: [
|
20
|
-
# { file: $stdout, format: :console },
|
21
|
-
# { file: "logs/app.log", format: :json }
|
22
|
-
# ]
|
23
|
-
# )
|
24
|
-
#
|
25
|
-
# @example Advanced configuration with filtering and formatting
|
26
|
-
# sink = UnifiedFileSink.new(
|
27
|
-
# configurations: [
|
28
|
-
# {
|
29
|
-
# file: $stdout,
|
30
|
-
# format: :console,
|
31
|
-
# show_timestamp: true,
|
32
|
-
# show_level: true
|
33
|
-
# },
|
34
|
-
# {
|
35
|
-
# file: "logs/debug.log",
|
36
|
-
# format: :text,
|
37
|
-
# min_level: :debug,
|
38
|
-
# include_context: true
|
39
|
-
# },
|
40
|
-
# {
|
41
|
-
# file: "logs/audit.log",
|
42
|
-
# format: :json,
|
43
|
-
# min_level: :info,
|
44
|
-
# include_metadata: true
|
45
|
-
# }
|
46
|
-
# ]
|
47
|
-
# )
|
48
|
-
class UnifiedFileSink < Base
|
49
|
-
# @return [Array<Hash>] list of output configurations
|
50
|
-
attr_reader :configurations
|
51
|
-
|
52
|
-
# Initialize unified file sink
|
53
|
-
#
|
54
|
-
# @param configurations [Array<Hash>] list of output configurations
|
55
|
-
def initialize(configurations: [])
|
56
|
-
@configurations = validate_configurations(configurations)
|
57
|
-
@formatters = {}
|
58
|
-
@files = {}
|
59
|
-
|
60
|
-
# Pre-initialize formatters and files
|
61
|
-
initialize_formatters
|
62
|
-
initialize_files
|
63
|
-
end
|
64
|
-
|
65
|
-
# Execute sink logic to write log entry to configured outputs
|
66
|
-
#
|
67
|
-
# @param log_request [LogRequest] the log request to process
|
68
|
-
# @yield [LogRequest] yields the processed log request to the next sink
|
69
|
-
# @yieldreturn [LogRequest] the processed log request
|
70
|
-
# @return [LogRequest] the final processed log request
|
71
|
-
def call(log_request, &block)
|
72
|
-
write_to_outputs(log_request)
|
73
|
-
block&.call(log_request) if block_given?
|
74
|
-
log_request
|
75
|
-
end
|
76
|
-
|
77
|
-
# Get sink configuration
|
78
|
-
#
|
79
|
-
# @return [Hash] sink configuration
|
80
|
-
def config
|
81
|
-
{
|
82
|
-
name: self.class.name.split("::").last,
|
83
|
-
configurations_count: @configurations.length,
|
84
|
-
configurations: @configurations.map { |config| sanitize_config(config) },
|
85
|
-
}
|
86
|
-
end
|
87
|
-
|
88
|
-
private
|
89
|
-
|
90
|
-
# Validate configuration array
|
91
|
-
#
|
92
|
-
# @param configurations [Array<Hash>] configurations to validate
|
93
|
-
# @return [Array<Hash>] validated configurations
|
94
|
-
# @raise [ArgumentError] if configuration is invalid
|
95
|
-
def validate_configurations(configurations)
|
96
|
-
raise ArgumentError, "configurations must be an array" unless configurations.is_a?(Array)
|
97
|
-
raise ArgumentError, "at least one configuration is required" if configurations.empty?
|
98
|
-
|
99
|
-
configurations.each_with_index do |config, index|
|
100
|
-
validate_single_configuration(config, index)
|
101
|
-
end
|
102
|
-
|
103
|
-
configurations
|
104
|
-
end
|
105
|
-
|
106
|
-
# Validate a single configuration
|
107
|
-
#
|
108
|
-
# @param config [Hash] configuration to validate
|
109
|
-
# @param index [Integer] configuration index for error messages
|
110
|
-
# @raise [ArgumentError] if configuration is invalid
|
111
|
-
def validate_single_configuration(config, index)
|
112
|
-
raise ArgumentError, "configuration #{index} must be a hash" unless config.is_a?(Hash)
|
113
|
-
|
114
|
-
required_keys = %i[file format]
|
115
|
-
missing_keys = required_keys - config.keys
|
116
|
-
unless missing_keys.empty?
|
117
|
-
raise ArgumentError,
|
118
|
-
"configuration #{index} missing required keys: #{missing_keys.join(", ")}"
|
119
|
-
end
|
120
|
-
|
121
|
-
# Validate file
|
122
|
-
file = config[:file]
|
123
|
-
unless file.is_a?(String) || file.is_a?(IO) || file.respond_to?(:write)
|
124
|
-
raise ArgumentError, "configuration #{index} file must be a String path, IO object, or writable object"
|
125
|
-
end
|
126
|
-
|
127
|
-
# Validate format
|
128
|
-
format = config[:format]
|
129
|
-
unless format.is_a?(Symbol) || format.is_a?(String)
|
130
|
-
raise ArgumentError, "configuration #{index} format must be a Symbol or String"
|
131
|
-
end
|
132
|
-
|
133
|
-
# Validate log levels if specified
|
134
|
-
if config[:min_level] && !valid_log_level?(config[:min_level])
|
135
|
-
raise ArgumentError, "configuration #{index} min_level must be a valid log level"
|
136
|
-
end
|
137
|
-
|
138
|
-
return unless config[:max_level] && !valid_log_level?(config[:max_level])
|
139
|
-
|
140
|
-
raise ArgumentError, "configuration #{index} max_level must be a valid log level"
|
141
|
-
end
|
142
|
-
|
143
|
-
# Check if log level is valid
|
144
|
-
#
|
145
|
-
# @param level [Symbol] log level to check
|
146
|
-
# @return [Boolean] true if valid
|
147
|
-
def valid_log_level?(level)
|
148
|
-
%i[debug info warn error fatal success].include?(level)
|
149
|
-
end
|
150
|
-
|
151
|
-
# Initialize formatters for each configuration
|
152
|
-
#
|
153
|
-
# @return [void]
|
154
|
-
def initialize_formatters
|
155
|
-
@configurations.each_with_index do |config, index|
|
156
|
-
format = config[:format].to_sym
|
157
|
-
formatter_options = config[:formatter_options] || {}
|
158
|
-
|
159
|
-
# Merge configuration options into formatter options
|
160
|
-
formatter_options[:include_context] = config[:include_context] if config.key?(:include_context)
|
161
|
-
formatter_options[:include_metadata] = config[:include_metadata] if config.key?(:include_metadata)
|
162
|
-
formatter_options[:show_timestamp] = config[:show_timestamp] if config.key?(:show_timestamp)
|
163
|
-
formatter_options[:show_level] = config[:show_level] if config.key?(:show_level)
|
164
|
-
|
165
|
-
begin
|
166
|
-
formatter_class = FormatRegistry.get(format)
|
167
|
-
|
168
|
-
# Handle different formatter initialization patterns
|
169
|
-
@formatters[index] = if formatter_class == Formatters::ConsoleFormatter
|
170
|
-
# ConsoleFormatter expects keyword arguments
|
171
|
-
formatter_class.new(
|
172
|
-
show_timestamp: formatter_options[:show_timestamp] || false,
|
173
|
-
show_level: formatter_options[:show_level] || false,
|
174
|
-
)
|
175
|
-
elsif formatter_class == Formatters::TextFormatter
|
176
|
-
# TextFormatter expects keyword arguments
|
177
|
-
formatter_class.new(
|
178
|
-
timestamp_format: formatter_options[:timestamp_format] || "%Y-%m-%d %H:%M:%S",
|
179
|
-
include_context: formatter_options[:include_context] || false,
|
180
|
-
)
|
181
|
-
elsif formatter_class == Formatters::JsonFormatter
|
182
|
-
# JsonFormatter expects keyword arguments
|
183
|
-
formatter_class.new(
|
184
|
-
options: formatter_options,
|
185
|
-
)
|
186
|
-
elsif formatter_class == Formatters::PlainTextFormatter
|
187
|
-
# PlainTextFormatter expects keyword arguments
|
188
|
-
formatter_class.new(
|
189
|
-
include_context: formatter_options[:include_context] || false,
|
190
|
-
)
|
191
|
-
elsif formatter_options.empty?
|
192
|
-
# Other formatters expect hash or no arguments
|
193
|
-
formatter_class.new
|
194
|
-
else
|
195
|
-
formatter_class.new(formatter_options)
|
196
|
-
end
|
197
|
-
rescue ArgumentError => e
|
198
|
-
raise ArgumentError, "configuration #{index} has invalid format '#{format}': #{e.message}"
|
199
|
-
end
|
200
|
-
end
|
201
|
-
end
|
202
|
-
|
203
|
-
# Initialize file handles for each configuration
|
204
|
-
#
|
205
|
-
# @return [void]
|
206
|
-
def initialize_files
|
207
|
-
@configurations.each_with_index do |config, index|
|
208
|
-
file = config[:file]
|
209
|
-
|
210
|
-
if file.is_a?(String)
|
211
|
-
# Ensure directory exists
|
212
|
-
FileUtils.mkdir_p(File.dirname(file)) unless File.dirname(file) == "."
|
213
|
-
|
214
|
-
# Open file with appropriate mode
|
215
|
-
mode = config[:append] == false ? "w" : "a"
|
216
|
-
@files[index] = File.open(file, mode)
|
217
|
-
else
|
218
|
-
# Use IO object directly
|
219
|
-
@files[index] = file
|
220
|
-
end
|
221
|
-
end
|
222
|
-
end
|
223
|
-
|
224
|
-
# Write log request to all applicable outputs
|
225
|
-
#
|
226
|
-
# @param log_request [LogRequest] the log request to write
|
227
|
-
def write_to_outputs(log_request)
|
228
|
-
@configurations.each_with_index do |config, index|
|
229
|
-
next unless should_output?(log_request, config)
|
230
|
-
|
231
|
-
formatter = @formatters[index]
|
232
|
-
file = @files[index]
|
233
|
-
|
234
|
-
formatted_message = formatter.format(log_request)
|
235
|
-
file.write(formatted_message)
|
236
|
-
file.write("\n") unless config[:no_newline]
|
237
|
-
file.flush
|
238
|
-
end
|
239
|
-
end
|
240
|
-
|
241
|
-
# Check if log request should be output to this configuration
|
242
|
-
#
|
243
|
-
# @param log_request [LogRequest] the log request
|
244
|
-
# @param config [Hash] the output configuration
|
245
|
-
# @return [Boolean] true if should output
|
246
|
-
def should_output?(log_request, config)
|
247
|
-
# Check level filtering
|
248
|
-
return false if config[:min_level] && !level_greater_or_equal?(log_request.level, config[:min_level])
|
249
|
-
|
250
|
-
return false if config[:max_level] && !level_less_or_equal?(log_request.level, config[:max_level])
|
251
|
-
|
252
|
-
# Check custom condition
|
253
|
-
return false if config[:condition] && !config[:condition].call(log_request)
|
254
|
-
|
255
|
-
true
|
256
|
-
end
|
257
|
-
|
258
|
-
# Check if log level is greater than or equal to minimum level
|
259
|
-
#
|
260
|
-
# @param level [Symbol] log level to check
|
261
|
-
# @param min_level [Symbol] minimum level
|
262
|
-
# @return [Boolean] true if level >= min_level
|
263
|
-
def level_greater_or_equal?(level, min_level)
|
264
|
-
levels = %i[debug info warn error fatal success]
|
265
|
-
levels.index(level) >= levels.index(min_level)
|
266
|
-
end
|
267
|
-
|
268
|
-
# Check if log level is less than or equal to maximum level
|
269
|
-
#
|
270
|
-
# @param level [Symbol] log level to check
|
271
|
-
# @param max_level [Symbol] maximum level
|
272
|
-
# @return [Boolean] true if level <= max_level
|
273
|
-
def level_less_or_equal?(level, max_level)
|
274
|
-
levels = %i[debug info warn error fatal success]
|
275
|
-
levels.index(level) <= levels.index(max_level)
|
276
|
-
end
|
277
|
-
|
278
|
-
# Sanitize configuration for display (remove sensitive data)
|
279
|
-
#
|
280
|
-
# @param config [Hash] configuration to sanitize
|
281
|
-
# @return [Hash] sanitized configuration
|
282
|
-
def sanitize_config(config)
|
283
|
-
sanitized = config.dup
|
284
|
-
|
285
|
-
# Remove condition procs and other non-serializable items
|
286
|
-
sanitized.delete(:condition)
|
287
|
-
|
288
|
-
# Convert IO objects to string representation
|
289
|
-
sanitized[:file] = "<#{sanitized[:file].class.name}>" if sanitized[:file].is_a?(IO)
|
290
|
-
|
291
|
-
sanitized
|
292
|
-
end
|
293
|
-
end
|
294
|
-
end
|
295
|
-
end
|
296
|
-
end
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "base"
|
4
|
+
require_relative "../format_registry"
|
5
|
+
require "fileutils"
|
6
|
+
|
7
|
+
module Makit
|
8
|
+
module Logging
|
9
|
+
module Sinks
|
10
|
+
# Unified file sink with configurable multiple outputs
|
11
|
+
#
|
12
|
+
# This sink replaces the need for multiple separate sinks by providing
|
13
|
+
# a single, highly configurable sink that can write to multiple files
|
14
|
+
# with different formats, filtering, and options. It supports console
|
15
|
+
# output, file logging, and structured logging through configuration.
|
16
|
+
#
|
17
|
+
# @example Basic usage with console and file output
|
18
|
+
# sink = UnifiedFileSink.new(
|
19
|
+
# configurations: [
|
20
|
+
# { file: $stdout, format: :console },
|
21
|
+
# { file: "logs/app.log", format: :json }
|
22
|
+
# ]
|
23
|
+
# )
|
24
|
+
#
|
25
|
+
# @example Advanced configuration with filtering and formatting
|
26
|
+
# sink = UnifiedFileSink.new(
|
27
|
+
# configurations: [
|
28
|
+
# {
|
29
|
+
# file: $stdout,
|
30
|
+
# format: :console,
|
31
|
+
# show_timestamp: true,
|
32
|
+
# show_level: true
|
33
|
+
# },
|
34
|
+
# {
|
35
|
+
# file: "logs/debug.log",
|
36
|
+
# format: :text,
|
37
|
+
# min_level: :debug,
|
38
|
+
# include_context: true
|
39
|
+
# },
|
40
|
+
# {
|
41
|
+
# file: "logs/audit.log",
|
42
|
+
# format: :json,
|
43
|
+
# min_level: :info,
|
44
|
+
# include_metadata: true
|
45
|
+
# }
|
46
|
+
# ]
|
47
|
+
# )
|
48
|
+
class UnifiedFileSink < Base
|
49
|
+
# @return [Array<Hash>] list of output configurations
|
50
|
+
attr_reader :configurations
|
51
|
+
|
52
|
+
# Initialize unified file sink
|
53
|
+
#
|
54
|
+
# @param configurations [Array<Hash>] list of output configurations
|
55
|
+
def initialize(configurations: [])
|
56
|
+
@configurations = validate_configurations(configurations)
|
57
|
+
@formatters = {}
|
58
|
+
@files = {}
|
59
|
+
|
60
|
+
# Pre-initialize formatters and files
|
61
|
+
initialize_formatters
|
62
|
+
initialize_files
|
63
|
+
end
|
64
|
+
|
65
|
+
# Execute sink logic to write log entry to configured outputs
|
66
|
+
#
|
67
|
+
# @param log_request [LogRequest] the log request to process
|
68
|
+
# @yield [LogRequest] yields the processed log request to the next sink
|
69
|
+
# @yieldreturn [LogRequest] the processed log request
|
70
|
+
# @return [LogRequest] the final processed log request
|
71
|
+
def call(log_request, &block)
|
72
|
+
write_to_outputs(log_request)
|
73
|
+
block&.call(log_request) if block_given?
|
74
|
+
log_request
|
75
|
+
end
|
76
|
+
|
77
|
+
# Get sink configuration
|
78
|
+
#
|
79
|
+
# @return [Hash] sink configuration
|
80
|
+
def config
|
81
|
+
{
|
82
|
+
name: self.class.name.split("::").last,
|
83
|
+
configurations_count: @configurations.length,
|
84
|
+
configurations: @configurations.map { |config| sanitize_config(config) },
|
85
|
+
}
|
86
|
+
end
|
87
|
+
|
88
|
+
private
|
89
|
+
|
90
|
+
# Validate configuration array
|
91
|
+
#
|
92
|
+
# @param configurations [Array<Hash>] configurations to validate
|
93
|
+
# @return [Array<Hash>] validated configurations
|
94
|
+
# @raise [ArgumentError] if configuration is invalid
|
95
|
+
def validate_configurations(configurations)
|
96
|
+
raise ArgumentError, "configurations must be an array" unless configurations.is_a?(Array)
|
97
|
+
raise ArgumentError, "at least one configuration is required" if configurations.empty?
|
98
|
+
|
99
|
+
configurations.each_with_index do |config, index|
|
100
|
+
validate_single_configuration(config, index)
|
101
|
+
end
|
102
|
+
|
103
|
+
configurations
|
104
|
+
end
|
105
|
+
|
106
|
+
# Validate a single configuration
|
107
|
+
#
|
108
|
+
# @param config [Hash] configuration to validate
|
109
|
+
# @param index [Integer] configuration index for error messages
|
110
|
+
# @raise [ArgumentError] if configuration is invalid
|
111
|
+
def validate_single_configuration(config, index)
|
112
|
+
raise ArgumentError, "configuration #{index} must be a hash" unless config.is_a?(Hash)
|
113
|
+
|
114
|
+
required_keys = %i[file format]
|
115
|
+
missing_keys = required_keys - config.keys
|
116
|
+
unless missing_keys.empty?
|
117
|
+
raise ArgumentError,
|
118
|
+
"configuration #{index} missing required keys: #{missing_keys.join(", ")}"
|
119
|
+
end
|
120
|
+
|
121
|
+
# Validate file
|
122
|
+
file = config[:file]
|
123
|
+
unless file.is_a?(String) || file.is_a?(IO) || file.respond_to?(:write)
|
124
|
+
raise ArgumentError, "configuration #{index} file must be a String path, IO object, or writable object"
|
125
|
+
end
|
126
|
+
|
127
|
+
# Validate format
|
128
|
+
format = config[:format]
|
129
|
+
unless format.is_a?(Symbol) || format.is_a?(String)
|
130
|
+
raise ArgumentError, "configuration #{index} format must be a Symbol or String"
|
131
|
+
end
|
132
|
+
|
133
|
+
# Validate log levels if specified
|
134
|
+
if config[:min_level] && !valid_log_level?(config[:min_level])
|
135
|
+
raise ArgumentError, "configuration #{index} min_level must be a valid log level"
|
136
|
+
end
|
137
|
+
|
138
|
+
return unless config[:max_level] && !valid_log_level?(config[:max_level])
|
139
|
+
|
140
|
+
raise ArgumentError, "configuration #{index} max_level must be a valid log level"
|
141
|
+
end
|
142
|
+
|
143
|
+
# Check if log level is valid
|
144
|
+
#
|
145
|
+
# @param level [Symbol] log level to check
|
146
|
+
# @return [Boolean] true if valid
|
147
|
+
def valid_log_level?(level)
|
148
|
+
%i[debug info warn error fatal success].include?(level)
|
149
|
+
end
|
150
|
+
|
151
|
+
# Initialize formatters for each configuration
|
152
|
+
#
|
153
|
+
# @return [void]
|
154
|
+
def initialize_formatters
|
155
|
+
@configurations.each_with_index do |config, index|
|
156
|
+
format = config[:format].to_sym
|
157
|
+
formatter_options = config[:formatter_options] || {}
|
158
|
+
|
159
|
+
# Merge configuration options into formatter options
|
160
|
+
formatter_options[:include_context] = config[:include_context] if config.key?(:include_context)
|
161
|
+
formatter_options[:include_metadata] = config[:include_metadata] if config.key?(:include_metadata)
|
162
|
+
formatter_options[:show_timestamp] = config[:show_timestamp] if config.key?(:show_timestamp)
|
163
|
+
formatter_options[:show_level] = config[:show_level] if config.key?(:show_level)
|
164
|
+
|
165
|
+
begin
|
166
|
+
formatter_class = FormatRegistry.get(format)
|
167
|
+
|
168
|
+
# Handle different formatter initialization patterns
|
169
|
+
@formatters[index] = if formatter_class == Formatters::ConsoleFormatter
|
170
|
+
# ConsoleFormatter expects keyword arguments
|
171
|
+
formatter_class.new(
|
172
|
+
show_timestamp: formatter_options[:show_timestamp] || false,
|
173
|
+
show_level: formatter_options[:show_level] || false,
|
174
|
+
)
|
175
|
+
elsif formatter_class == Formatters::TextFormatter
|
176
|
+
# TextFormatter expects keyword arguments
|
177
|
+
formatter_class.new(
|
178
|
+
timestamp_format: formatter_options[:timestamp_format] || "%Y-%m-%d %H:%M:%S",
|
179
|
+
include_context: formatter_options[:include_context] || false,
|
180
|
+
)
|
181
|
+
elsif formatter_class == Formatters::JsonFormatter
|
182
|
+
# JsonFormatter expects keyword arguments
|
183
|
+
formatter_class.new(
|
184
|
+
options: formatter_options,
|
185
|
+
)
|
186
|
+
elsif formatter_class == Formatters::PlainTextFormatter
|
187
|
+
# PlainTextFormatter expects keyword arguments
|
188
|
+
formatter_class.new(
|
189
|
+
include_context: formatter_options[:include_context] || false,
|
190
|
+
)
|
191
|
+
elsif formatter_options.empty?
|
192
|
+
# Other formatters expect hash or no arguments
|
193
|
+
formatter_class.new
|
194
|
+
else
|
195
|
+
formatter_class.new(formatter_options)
|
196
|
+
end
|
197
|
+
rescue ArgumentError => e
|
198
|
+
raise ArgumentError, "configuration #{index} has invalid format '#{format}': #{e.message}"
|
199
|
+
end
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
# Initialize file handles for each configuration
|
204
|
+
#
|
205
|
+
# @return [void]
|
206
|
+
def initialize_files
|
207
|
+
@configurations.each_with_index do |config, index|
|
208
|
+
file = config[:file]
|
209
|
+
|
210
|
+
if file.is_a?(String)
|
211
|
+
# Ensure directory exists
|
212
|
+
FileUtils.mkdir_p(File.dirname(file)) unless File.dirname(file) == "."
|
213
|
+
|
214
|
+
# Open file with appropriate mode
|
215
|
+
mode = config[:append] == false ? "w" : "a"
|
216
|
+
@files[index] = File.open(file, mode)
|
217
|
+
else
|
218
|
+
# Use IO object directly
|
219
|
+
@files[index] = file
|
220
|
+
end
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
# Write log request to all applicable outputs
|
225
|
+
#
|
226
|
+
# @param log_request [LogRequest] the log request to write
|
227
|
+
def write_to_outputs(log_request)
|
228
|
+
@configurations.each_with_index do |config, index|
|
229
|
+
next unless should_output?(log_request, config)
|
230
|
+
|
231
|
+
formatter = @formatters[index]
|
232
|
+
file = @files[index]
|
233
|
+
|
234
|
+
formatted_message = formatter.format(log_request)
|
235
|
+
file.write(formatted_message)
|
236
|
+
file.write("\n") unless config[:no_newline]
|
237
|
+
file.flush
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
241
|
+
# Check if log request should be output to this configuration
|
242
|
+
#
|
243
|
+
# @param log_request [LogRequest] the log request
|
244
|
+
# @param config [Hash] the output configuration
|
245
|
+
# @return [Boolean] true if should output
|
246
|
+
def should_output?(log_request, config)
|
247
|
+
# Check level filtering
|
248
|
+
return false if config[:min_level] && !level_greater_or_equal?(log_request.level, config[:min_level])
|
249
|
+
|
250
|
+
return false if config[:max_level] && !level_less_or_equal?(log_request.level, config[:max_level])
|
251
|
+
|
252
|
+
# Check custom condition
|
253
|
+
return false if config[:condition] && !config[:condition].call(log_request)
|
254
|
+
|
255
|
+
true
|
256
|
+
end
|
257
|
+
|
258
|
+
# Check if log level is greater than or equal to minimum level
|
259
|
+
#
|
260
|
+
# @param level [Symbol] log level to check
|
261
|
+
# @param min_level [Symbol] minimum level
|
262
|
+
# @return [Boolean] true if level >= min_level
|
263
|
+
def level_greater_or_equal?(level, min_level)
|
264
|
+
levels = %i[debug info warn error fatal success]
|
265
|
+
levels.index(level) >= levels.index(min_level)
|
266
|
+
end
|
267
|
+
|
268
|
+
# Check if log level is less than or equal to maximum level
|
269
|
+
#
|
270
|
+
# @param level [Symbol] log level to check
|
271
|
+
# @param max_level [Symbol] maximum level
|
272
|
+
# @return [Boolean] true if level <= max_level
|
273
|
+
def level_less_or_equal?(level, max_level)
|
274
|
+
levels = %i[debug info warn error fatal success]
|
275
|
+
levels.index(level) <= levels.index(max_level)
|
276
|
+
end
|
277
|
+
|
278
|
+
# Sanitize configuration for display (remove sensitive data)
|
279
|
+
#
|
280
|
+
# @param config [Hash] configuration to sanitize
|
281
|
+
# @return [Hash] sanitized configuration
|
282
|
+
def sanitize_config(config)
|
283
|
+
sanitized = config.dup
|
284
|
+
|
285
|
+
# Remove condition procs and other non-serializable items
|
286
|
+
sanitized.delete(:condition)
|
287
|
+
|
288
|
+
# Convert IO objects to string representation
|
289
|
+
sanitized[:file] = "<#{sanitized[:file].class.name}>" if sanitized[:file].is_a?(IO)
|
290
|
+
|
291
|
+
sanitized
|
292
|
+
end
|
293
|
+
end
|
294
|
+
end
|
295
|
+
end
|
296
|
+
end
|