featurevisor 0.3.0 → 1.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.
@@ -5,7 +5,7 @@ require "json"
5
5
  module Featurevisor
6
6
  # DatafileReader class for reading and processing Featurevisor datafiles
7
7
  class DatafileReader
8
- attr_reader :schema_version, :revision, :segments, :features, :logger, :regex_cache
8
+ attr_reader :schema_version, :revision, :featurevisor_version, :segments, :features, :logger, :regex_cache
9
9
 
10
10
  # Initialize a new DatafileReader
11
11
  # @param options [Hash] Options hash containing datafile and logger
@@ -17,6 +17,7 @@ module Featurevisor
17
17
 
18
18
  @schema_version = datafile[:schemaVersion]
19
19
  @revision = datafile[:revision]
20
+ @featurevisor_version = datafile[:featurevisorVersion]
20
21
  @segments = (datafile[:segments] || {}).transform_keys(&:to_sym)
21
22
  @features = (datafile[:features] || {}).transform_keys(&:to_sym)
22
23
 
@@ -65,6 +66,16 @@ module Featurevisor
65
66
  @schema_version
66
67
  end
67
68
 
69
+ def get_datafile
70
+ {
71
+ schemaVersion: @schema_version,
72
+ revision: @revision,
73
+ featurevisorVersion: @featurevisor_version,
74
+ segments: @segments,
75
+ features: @features
76
+ }.compact
77
+ end
78
+
68
79
  # Get a segment by key
69
80
  # @param segment_key [String] Segment key to retrieve
70
81
  # @return [Hash, nil] Segment data or nil if not found
@@ -172,15 +183,11 @@ module Featurevisor
172
183
  end
173
184
 
174
185
  if conditions.is_a?(Hash) && conditions[:not] && conditions[:not].is_a?(Array)
175
- return conditions[:not].all? do
176
- all_conditions_are_matched({ and: conditions[:not] }, context) == false
177
- end
186
+ return !all_conditions_are_matched({ and: conditions[:not] }, context)
178
187
  end
179
188
 
180
189
  if conditions.is_a?(Hash) && conditions["not"] && conditions["not"].is_a?(Array)
181
- return conditions["not"].all? do
182
- all_conditions_are_matched({ "and" => conditions["not"] }, context) == false
183
- end
190
+ return !all_conditions_are_matched({ "and" => conditions["not"] }, context)
184
191
  end
185
192
 
186
193
 
@@ -237,15 +244,11 @@ module Featurevisor
237
244
  end
238
245
 
239
246
  if group_segments[:not] && group_segments[:not].is_a?(Array)
240
- return group_segments[:not].all? do
241
- all_segments_are_matched({ and: group_segments[:not] }, context) == false
242
- end
247
+ return !all_segments_are_matched({ and: group_segments[:not] }, context)
243
248
  end
244
249
 
245
250
  if group_segments["not"] && group_segments["not"].is_a?(Array)
246
- return group_segments["not"].all? do
247
- all_segments_are_matched({ "and" => group_segments["not"] }, context) == false
248
- end
251
+ return !all_segments_are_matched({ "and" => group_segments["not"] }, context)
249
252
  end
250
253
 
251
254
 
@@ -351,4 +354,6 @@ module Featurevisor
351
354
  segments
352
355
  end
353
356
  end
357
+
358
+ private_constant :DatafileReader
354
359
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Featurevisor
4
4
  # Event names for the emitter
5
- EVENT_NAMES = %w[datafile_set context_set sticky_set].freeze
5
+ EVENT_NAMES = %w[datafile_set context_set sticky_set error].freeze
6
6
 
7
7
  # Event emitter class for handling event subscriptions and triggers
8
8
  class Emitter
@@ -42,7 +42,7 @@ module Featurevisor
42
42
 
43
43
  return unless listeners
44
44
 
45
- listeners.each do |listener|
45
+ listeners.dup.each do |listener|
46
46
  begin
47
47
  listener.call(details)
48
48
  rescue => err
@@ -36,20 +36,17 @@ module Featurevisor
36
36
  # Evaluation module for feature flag evaluation
37
37
  module Evaluate
38
38
 
39
- # Evaluate with hooks
39
+ # Evaluate with modules
40
40
  # @param options [Hash] Evaluation options
41
41
  # @return [Hash] Evaluation result
42
- def self.evaluate_with_hooks(options)
42
+ def self.evaluate_with_modules(options)
43
43
  begin
44
- hooks_manager = options[:hooks_manager]
45
- hooks = hooks_manager.get_all
44
+ modules_manager = options[:modules_manager]
46
45
 
47
- # Run before hooks
46
+ # Run before modules
48
47
  result_options = options
49
- hooks.each do |hook|
50
- if hook.respond_to?(:call_before)
51
- result_options = hook.call_before(result_options)
52
- end
48
+ if modules_manager
49
+ result_options = modules_manager.run_before_modules(result_options)
53
50
  end
54
51
 
55
52
  # Evaluate
@@ -69,11 +66,9 @@ module Featurevisor
69
66
  evaluation[:variable_value] = options[:default_variable_value]
70
67
  end
71
68
 
72
- # Run after hooks
73
- hooks.each do |hook|
74
- if hook.respond_to?(:call_after)
75
- evaluation = hook.call_after(evaluation, result_options)
76
- end
69
+ # Run after modules
70
+ if modules_manager
71
+ evaluation = modules_manager.run_after_modules(evaluation, result_options)
77
72
  end
78
73
 
79
74
  evaluation
@@ -108,9 +103,7 @@ module Featurevisor
108
103
  logger = options[:logger]
109
104
  datafile_reader = options[:datafile_reader]
110
105
  sticky = options[:sticky]
111
- hooks_manager = options[:hooks_manager]
112
-
113
- hooks = hooks_manager.get_all
106
+ modules_manager = options[:modules_manager]
114
107
  evaluation = nil
115
108
 
116
109
  begin
@@ -422,24 +415,24 @@ module Featurevisor
422
415
  logger: logger
423
416
  })
424
417
 
425
- # Run bucket key hooks
426
- bucket_key = hooks_manager.run_bucket_key_hooks({
418
+ # Run bucket key modules
419
+ bucket_key = modules_manager.run_bucket_key_modules({
427
420
  feature_key: feature_key,
428
421
  context: context,
429
422
  bucket_by: feature[:bucketBy],
430
423
  bucket_key: bucket_key
431
- })
424
+ }) if modules_manager
432
425
 
433
426
  # bucketValue
434
427
  bucket_value = Featurevisor::Bucketer.get_bucketed_number(bucket_key)
435
428
 
436
- # Run bucket value hooks
437
- bucket_value = hooks_manager.run_bucket_value_hooks({
429
+ # Run bucket value modules
430
+ bucket_value = modules_manager.run_bucket_value_modules({
438
431
  feature_key: feature_key,
439
432
  bucket_key: bucket_key,
440
433
  context: context,
441
434
  bucket_value: bucket_value
442
- })
435
+ }) if modules_manager
443
436
 
444
437
  matched_traffic = nil
445
438
  matched_allocation = nil
@@ -24,7 +24,7 @@ module Featurevisor
24
24
  # @param previous_reader [DatafileReader] Previous datafile reader
25
25
  # @param new_reader [DatafileReader] New datafile reader
26
26
  # @return [Hash] Event parameters
27
- def self.get_params_for_datafile_set_event(previous_reader, new_reader)
27
+ def self.get_params_for_datafile_set_event(previous_reader, new_reader, replace = false)
28
28
  previous_revision = previous_reader.get_revision
29
29
  previous_feature_keys = previous_reader.get_feature_keys
30
30
 
@@ -67,9 +67,10 @@ module Featurevisor
67
67
 
68
68
  {
69
69
  revision: new_revision,
70
- previous_revision: previous_revision,
71
- revision_changed: previous_revision != new_revision,
72
- features: all_affected_features
70
+ previousRevision: previous_revision,
71
+ revisionChanged: previous_revision != new_revision,
72
+ features: all_affected_features,
73
+ replaced: replace
73
74
  }
74
75
  end
75
76
  end
@@ -1,12 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "json"
4
+ require "securerandom"
4
5
 
5
6
  module Featurevisor
6
7
  # Instance class for managing feature flag evaluations
7
8
  class Instance
8
- attr_reader :context, :logger, :sticky, :datafile_reader, :hooks_manager, :emitter
9
-
10
9
  # Empty datafile template
11
10
  EMPTY_DATAFILE = {
12
11
  schemaVersion: "2",
@@ -20,34 +19,44 @@ module Featurevisor
20
19
  # @option options [Hash, String] :datafile Datafile content or JSON string
21
20
  # @option options [Hash] :context Initial context
22
21
  # @option options [String] :log_level Log level
23
- # @option options [Logger] :logger Logger instance
24
22
  # @option options [Hash] :sticky Sticky features
25
- # @option options [Array<Hook>] :hooks Array of hooks
23
+ # @option options [Array<Hash, FeaturevisorModule>] :modules Array of modules
24
+ # @option options [Proc] :on_diagnostic Diagnostic handler
26
25
  def initialize(options = {})
27
26
  # from options
28
27
  @context = options[:context] || {}
29
- @logger = options[:logger] || Featurevisor.create_logger(level: options[:log_level] || "info")
30
- @hooks_manager = Featurevisor::Hooks::HooksManager.new(
31
- hooks: (options[:hooks] || []).map { |hook_data| Featurevisor::Hooks::Hook.new(hook_data) },
32
- logger: @logger
28
+ @logger = Logger.new(
29
+ level: options[:log_level] || "info",
30
+ handler: method(:handle_internal_log)
33
31
  )
32
+ @on_diagnostic = options[:on_diagnostic] || options[:onDiagnostic]
34
33
  @emitter = Featurevisor::Emitter.new
35
34
  @sticky = options[:sticky] || {}
35
+ @closed = false
36
+ @module_diagnostic_subscriptions = []
36
37
 
37
38
  # datafile
38
- @datafile_reader = Featurevisor::DatafileReader.new(
39
+ @datafile_reader = DatafileReader.new(
39
40
  datafile: EMPTY_DATAFILE,
40
41
  logger: @logger
41
42
  )
42
43
 
44
+ @modules_manager = Featurevisor::Modules::ModulesManager.new(
45
+ modules: options[:modules] || [],
46
+ report_diagnostic: method(:report_diagnostic),
47
+ module_api_factory: method(:create_module_api),
48
+ clear_module_diagnostic_subscriptions: method(:clear_module_diagnostic_subscriptions)
49
+ )
50
+
43
51
  if options[:datafile]
44
- @datafile_reader = Featurevisor::DatafileReader.new(
45
- datafile: options[:datafile].is_a?(String) ? JSON.parse(options[:datafile], symbolize_names: true) : options[:datafile],
46
- logger: @logger
47
- )
52
+ set_datafile(options[:datafile], true)
48
53
  end
49
54
 
50
- @logger.info("Featurevisor SDK initialized")
55
+ report_diagnostic(
56
+ level: "info",
57
+ code: "sdk_initialized",
58
+ message: "SDK initialized"
59
+ )
51
60
  end
52
61
 
53
62
  # Set the log level
@@ -56,22 +65,61 @@ module Featurevisor
56
65
  @logger.set_level(level)
57
66
  end
58
67
 
68
+ def handle_internal_log(level, message, details = nil)
69
+ details = (details || {}).dup
70
+ code = details[:reason] || details["reason"] || message
71
+ code = "deprecated_feature" if message == "feature is deprecated"
72
+ code = "deprecated_variable" if message == "variable is deprecated"
73
+ code = "feature_not_found" if message == "feature not found"
74
+ code = "variable_not_found" if message == "variable schema not found"
75
+ code = "no_variations" if message == "no variations"
76
+ code = "invalid_bucket_by" if message == "invalid bucketBy"
77
+ report_diagnostic(level: level, code: code.to_s, message: message, details: details)
78
+ end
79
+ private :handle_internal_log
80
+
59
81
  # Set the datafile
60
82
  # @param datafile [Hash, String] Datafile content or JSON string
61
- def set_datafile(datafile)
83
+ # @param replace [Boolean] Whether to replace instead of merge
84
+ def set_datafile(datafile, replace = false)
85
+ return if @closed
86
+
62
87
  begin
63
- new_datafile_reader = Featurevisor::DatafileReader.new(
64
- datafile: datafile.is_a?(String) ? JSON.parse(datafile, symbolize_names: true) : datafile,
88
+ parsed_datafile = if datafile.is_a?(String)
89
+ JSON.parse(datafile, symbolize_names: true)
90
+ elsif datafile.is_a?(Hash)
91
+ JSON.parse(JSON.generate(datafile), symbolize_names: true)
92
+ else
93
+ datafile
94
+ end
95
+ unless parsed_datafile.is_a?(Hash) && parsed_datafile[:schemaVersion].is_a?(String) &&
96
+ parsed_datafile[:revision].is_a?(String) && parsed_datafile[:segments].is_a?(Hash) &&
97
+ parsed_datafile[:features].is_a?(Hash)
98
+ raise ArgumentError, "Invalid datafile"
99
+ end
100
+ next_datafile = replace ? parsed_datafile : merge_datafiles(@datafile_reader.get_datafile, parsed_datafile)
101
+ new_datafile_reader = DatafileReader.new(
102
+ datafile: next_datafile,
65
103
  logger: @logger
66
104
  )
67
105
 
68
- details = Featurevisor::Events.get_params_for_datafile_set_event(@datafile_reader, new_datafile_reader)
106
+ details = Featurevisor::Events.get_params_for_datafile_set_event(@datafile_reader, new_datafile_reader, replace)
69
107
  @datafile_reader = new_datafile_reader
70
108
 
71
- @logger.info("datafile set", details)
109
+ report_diagnostic(
110
+ level: "info",
111
+ code: "datafile_set",
112
+ message: "Datafile set",
113
+ details: details
114
+ )
72
115
  @emitter.trigger("datafile_set", details)
73
116
  rescue => e
74
- @logger.error("could not parse datafile", { error: e })
117
+ report_diagnostic(
118
+ level: "error",
119
+ code: "invalid_datafile",
120
+ message: "Could not parse datafile",
121
+ original_error: e
122
+ )
75
123
  end
76
124
  end
77
125
 
@@ -92,7 +140,12 @@ module Featurevisor
92
140
 
93
141
  params = Featurevisor::Events.get_params_for_sticky_set_event(previous_sticky_features, @sticky, replace)
94
142
 
95
- @logger.info("sticky features set", params)
143
+ report_diagnostic(
144
+ level: "info",
145
+ code: "sticky_set",
146
+ message: "Sticky features set",
147
+ details: params
148
+ )
96
149
  @emitter.trigger("sticky_set", params)
97
150
  end
98
151
 
@@ -102,6 +155,26 @@ module Featurevisor
102
155
  @datafile_reader.get_revision
103
156
  end
104
157
 
158
+ def get_schema_version
159
+ @datafile_reader.get_schema_version
160
+ end
161
+
162
+ def get_segment(segment_key)
163
+ @datafile_reader.get_segment(segment_key)
164
+ end
165
+
166
+ def get_feature_keys
167
+ @datafile_reader.get_feature_keys
168
+ end
169
+
170
+ def get_variable_keys(feature_key)
171
+ @datafile_reader.get_variable_keys(feature_key)
172
+ end
173
+
174
+ def has_variations?(feature_key)
175
+ @datafile_reader.has_variations?(feature_key)
176
+ end
177
+
105
178
  # Get a feature by key
106
179
  # @param feature_key [String] Feature key
107
180
  # @return [Hash, nil] Feature data or nil if not found
@@ -109,11 +182,15 @@ module Featurevisor
109
182
  @datafile_reader.get_feature(feature_key)
110
183
  end
111
184
 
112
- # Add a hook
113
- # @param hook [Hook] Hook to add
114
- # @return [Proc, nil] Remove function or nil if hook already exists
115
- def add_hook(hook)
116
- @hooks_manager.add(hook)
185
+ # Add a module
186
+ # @param mod [Hash, FeaturevisorModule] Module to add
187
+ # @return [Proc, nil] Remove function or nil if module already exists
188
+ def add_module(mod)
189
+ @modules_manager.add(mod)
190
+ end
191
+
192
+ def remove_module(name_or_module)
193
+ @modules_manager.remove(name_or_module)
117
194
  end
118
195
 
119
196
  # Subscribe to an event
@@ -126,6 +203,9 @@ module Featurevisor
126
203
 
127
204
  # Close the instance
128
205
  def close
206
+ @closed = true
207
+ @modules_manager.close_all
208
+ @module_diagnostic_subscriptions = []
129
209
  @emitter.clear_all
130
210
  end
131
211
 
@@ -144,10 +224,15 @@ module Featurevisor
144
224
  replaced: replace
145
225
  })
146
226
 
147
- @logger.debug(replace ? "context replaced" : "context updated", {
148
- context: @context,
149
- replaced: replace
150
- })
227
+ report_diagnostic(
228
+ level: "debug",
229
+ code: "context_set",
230
+ message: replace ? "Context replaced" : "Context updated",
231
+ details: {
232
+ context: @context,
233
+ replaced: replace
234
+ }
235
+ )
151
236
  end
152
237
 
153
238
  # Get context
@@ -182,7 +267,7 @@ module Featurevisor
182
267
  # @param options [Hash] Override options
183
268
  # @return [Hash] Evaluation result
184
269
  def evaluate_flag(feature_key, context = {}, options = {})
185
- Featurevisor::Evaluate.evaluate_with_hooks(
270
+ Featurevisor::Evaluate.evaluate_with_modules(
186
271
  get_evaluation_dependencies(context, options).merge(
187
272
  type: "flag",
188
273
  feature_key: feature_key
@@ -211,7 +296,7 @@ module Featurevisor
211
296
  # @param options [Hash] Override options
212
297
  # @return [Hash] Evaluation result
213
298
  def evaluate_variation(feature_key, context = {}, options = {})
214
- Featurevisor::Evaluate.evaluate_with_hooks(
299
+ Featurevisor::Evaluate.evaluate_with_modules(
215
300
  get_evaluation_dependencies(context, options).merge(
216
301
  type: "variation",
217
302
  feature_key: feature_key
@@ -248,7 +333,7 @@ module Featurevisor
248
333
  # @param options [Hash] Override options
249
334
  # @return [Hash] Evaluation result
250
335
  def evaluate_variable(feature_key, variable_key, context = {}, options = {})
251
- Featurevisor::Evaluate.evaluate_with_hooks(
336
+ Featurevisor::Evaluate.evaluate_with_modules(
252
337
  get_evaluation_dependencies(context, options).merge(
253
338
  type: "variable",
254
339
  feature_key: feature_key,
@@ -417,9 +502,9 @@ module Featurevisor
417
502
  {
418
503
  context: get_context(context),
419
504
  logger: @logger,
420
- hooks_manager: @hooks_manager,
505
+ modules_manager: @modules_manager,
421
506
  datafile_reader: @datafile_reader,
422
- sticky: options[:sticky] ? { **(@sticky || {}), **options[:sticky] } : @sticky,
507
+ sticky: options[:__featurevisor_child_sticky] || @sticky,
423
508
  default_variation_value: options[:default_variation_value],
424
509
  default_variable_value: options[:default_variable_value]
425
510
  }
@@ -436,11 +521,12 @@ module Featurevisor
436
521
  when "string"
437
522
  value.is_a?(String) ? value : nil
438
523
  when "integer"
439
- value.is_a?(String) ? Integer(value, 10) : (value.is_a?(Integer) ? value : nil)
524
+ return value if value.is_a?(Integer)
525
+ value.is_a?(Float) && value.finite? && value == value.to_i ? value.to_i : nil
440
526
  when "double"
441
- value.is_a?(String) ? Float(value) : (value.is_a?(Numeric) ? value.to_f : nil)
527
+ value.is_a?(Numeric) && value.finite? ? value.to_f : nil
442
528
  when "boolean"
443
- value == true
529
+ value == true || value == false ? value : nil
444
530
  when "array"
445
531
  value.is_a?(Array) ? value : nil
446
532
  when "object"
@@ -452,12 +538,112 @@ module Featurevisor
452
538
  rescue
453
539
  nil
454
540
  end
541
+
542
+ def merge_datafiles(previous, incoming)
543
+ previous ||= EMPTY_DATAFILE
544
+ incoming ||= EMPTY_DATAFILE
545
+
546
+ {
547
+ schemaVersion: incoming[:schemaVersion],
548
+ revision: incoming[:revision],
549
+ featurevisorVersion: incoming[:featurevisorVersion],
550
+ segments: {
551
+ **(previous[:segments] || {}),
552
+ **(incoming[:segments] || {})
553
+ },
554
+ features: {
555
+ **(previous[:features] || {}),
556
+ **(incoming[:features] || {})
557
+ }
558
+ }.compact
559
+ end
560
+
561
+ def create_module_api(mod)
562
+ instance = self
563
+ {
564
+ get_revision: -> { instance.get_revision },
565
+ on_diagnostic: lambda do |handler, options = {}|
566
+ subscription = {
567
+ id: SecureRandom.uuid,
568
+ module_id: mod.id,
569
+ handler: handler,
570
+ level: options[:level] || options[:log_level] || "info"
571
+ }
572
+ @module_diagnostic_subscriptions << subscription
573
+ -> { @module_diagnostic_subscriptions.reject! { |item| item[:id] == subscription[:id] } }
574
+ end,
575
+ report_diagnostic: ->(diagnostic) { report_diagnostic(diagnostic, mod) }
576
+ }
577
+ end
578
+
579
+ def clear_module_diagnostic_subscriptions(mod)
580
+ @module_diagnostic_subscriptions.reject! { |item| item[:module_id] == mod.id }
581
+ end
582
+
583
+ def report_diagnostic(diagnostic, source_module = nil)
584
+ diagnostic = (diagnostic || {}).dup
585
+ diagnostic[:level] ||= "info"
586
+ diagnostic[:module] = source_module.name if source_module && source_module.name
587
+ details = (diagnostic[:details] || {}).dup
588
+ legacy_module_name = diagnostic.delete(:module_name)
589
+ legacy_original_error = diagnostic.delete(:original_error)
590
+ diagnostic[:moduleName] = legacy_module_name if !diagnostic.key?(:moduleName) && !legacy_module_name.nil?
591
+ diagnostic[:originalError] = legacy_original_error if !diagnostic.key?(:originalError) && !legacy_original_error.nil?
592
+ diagnostic.each do |key, value|
593
+ next if %i[level code message module moduleName originalError details].include?(key)
594
+
595
+ details[key] = value
596
+ end
597
+ diagnostic.select! { |key, _| %i[level code message module moduleName originalError details].include?(key) }
598
+ diagnostic[:details] = details
599
+
600
+ @module_diagnostic_subscriptions.dup.each do |subscription|
601
+ next if source_module && subscription[:module_id] == source_module.id
602
+ next unless should_report_diagnostic?(diagnostic[:level], subscription[:level])
603
+
604
+ begin
605
+ subscription[:handler].call(diagnostic)
606
+ rescue => e
607
+ Kernel.warn("[Featurevisor] Diagnostic handler failed: #{e}")
608
+ end
609
+ end
610
+
611
+ if @on_diagnostic
612
+ if should_report_diagnostic?(diagnostic[:level], @logger.level)
613
+ begin
614
+ @on_diagnostic.call(diagnostic)
615
+ rescue => e
616
+ Kernel.warn("[Featurevisor] Diagnostic handler failed: #{e}")
617
+ end
618
+ end
619
+ else
620
+ Logger.new(level: @logger.level).log(
621
+ diagnostic[:level],
622
+ diagnostic[:message],
623
+ diagnostic
624
+ )
625
+ end
626
+
627
+ if diagnostic[:level] == "error"
628
+ @emitter.trigger("error", diagnostic: diagnostic)
629
+ end
630
+ end
631
+
632
+ def should_report_diagnostic?(diagnostic_level, subscriber_level)
633
+ levels = Featurevisor::LOG_LEVELS
634
+ diagnostic_index = levels.index(diagnostic_level || "info")
635
+ subscriber_index = levels.index(subscriber_level || "info")
636
+
637
+ return false if diagnostic_index.nil? || subscriber_index.nil?
638
+
639
+ subscriber_index >= diagnostic_index
640
+ end
455
641
  end
456
642
 
457
643
  # Create a new Featurevisor instance
458
644
  # @param options [Hash] Instance options
459
645
  # @return [Instance] New instance
460
- def self.create_instance(options = {})
646
+ def self.create_featurevisor(options = {})
461
647
  Instance.new(options)
462
648
  end
463
649
  end
@@ -113,13 +113,6 @@ module Featurevisor
113
113
  end
114
114
  end
115
115
 
116
- # Create a new logger instance
117
- # @param options [Hash] Logger options
118
- # @return [Logger] New logger instance
119
- def self.create_logger(options = {})
120
- Logger.new(options)
121
- end
122
-
123
116
  # Default log handler function
124
117
  # @param level [String] Log level
125
118
  # @param message [String] Log message