ez_logs_agent 0.1.3
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 +7 -0
- data/CHANGELOG.md +80 -0
- data/LICENSE.txt +21 -0
- data/README.md +1023 -0
- data/lib/ez_logs_agent/actor.rb +57 -0
- data/lib/ez_logs_agent/actor_validator.rb +58 -0
- data/lib/ez_logs_agent/buffer.rb +83 -0
- data/lib/ez_logs_agent/capturers/active_job_capturer.rb +300 -0
- data/lib/ez_logs_agent/capturers/database_capturer.rb +467 -0
- data/lib/ez_logs_agent/capturers/job_capturer.rb +261 -0
- data/lib/ez_logs_agent/configuration.rb +184 -0
- data/lib/ez_logs_agent/configuration_validator.rb +139 -0
- data/lib/ez_logs_agent/correlation.rb +40 -0
- data/lib/ez_logs_agent/event_builder.rb +281 -0
- data/lib/ez_logs_agent/flush_scheduler.rb +99 -0
- data/lib/ez_logs_agent/logger.rb +62 -0
- data/lib/ez_logs_agent/middleware/http_request.rb +992 -0
- data/lib/ez_logs_agent/railtie.rb +353 -0
- data/lib/ez_logs_agent/resource_extractor.rb +172 -0
- data/lib/ez_logs_agent/retry_sender.rb +120 -0
- data/lib/ez_logs_agent/sanitizer.rb +150 -0
- data/lib/ez_logs_agent/transport.rb +91 -0
- data/lib/ez_logs_agent/user_agent_detector.rb +51 -0
- data/lib/ez_logs_agent/version.rb +5 -0
- data/lib/ez_logs_agent.rb +44 -0
- data/lib/generators/ez_logs_agent/install/install_generator.rb +94 -0
- data/lib/generators/ez_logs_agent/install/templates/ez_logs_agent.rb.tt +128 -0
- data/lib/tasks/ez_logs_agent.rake +110 -0
- metadata +172 -0
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "time"
|
|
4
|
+
|
|
5
|
+
module EzLogsAgent
|
|
6
|
+
# EventBuilder constructs valid Event hashes according to the Event Structure contract.
|
|
7
|
+
#
|
|
8
|
+
# This is a pure, side-effect-free builder that:
|
|
9
|
+
# - Validates required fields
|
|
10
|
+
# - Validates enum values
|
|
11
|
+
# - Enforces conditional rules (outcome ↔ error_message)
|
|
12
|
+
# - Sanitizes sensitive data
|
|
13
|
+
# - Returns a valid Event hash
|
|
14
|
+
#
|
|
15
|
+
# EventBuilder does NOT:
|
|
16
|
+
# - Buffer or send Events
|
|
17
|
+
# - Generate correlation IDs
|
|
18
|
+
# - Interact with Rails or ActiveRecord
|
|
19
|
+
# - Mutate global state
|
|
20
|
+
#
|
|
21
|
+
# @see docs/agent/event_structure.md for complete specification
|
|
22
|
+
module EventBuilder
|
|
23
|
+
# Sensitive keys that should be filtered from source_data and context
|
|
24
|
+
SENSITIVE_KEYS = %w[password token secret api_key credit_card].freeze
|
|
25
|
+
|
|
26
|
+
# Valid source types
|
|
27
|
+
VALID_SOURCE_TYPES = %w[http_request background_job database_callback].freeze
|
|
28
|
+
|
|
29
|
+
# Valid outcome values
|
|
30
|
+
VALID_OUTCOMES = %w[success failure].freeze
|
|
31
|
+
|
|
32
|
+
# Builds a valid Event hash
|
|
33
|
+
#
|
|
34
|
+
# @param source_type [String, Symbol] Event source type (http_request, background_job, database_callback)
|
|
35
|
+
# @param source_data [Hash] Technical details about the event source
|
|
36
|
+
# @param outcome [String, Symbol] Event outcome (success, failure)
|
|
37
|
+
# @param correlation_id [String, nil] Optional correlation identifier
|
|
38
|
+
# @param resource_ids [Array<Hash>] Optional array of resource identifiers
|
|
39
|
+
# @param context [Hash, nil] Optional human-readable context
|
|
40
|
+
# @param duration_ms [Integer, nil] Optional operation duration in milliseconds
|
|
41
|
+
# @param error_message [String, nil] Optional error message (required if outcome is failure)
|
|
42
|
+
# @param timestamp [String, Time, nil] Optional timestamp (defaults to current time)
|
|
43
|
+
#
|
|
44
|
+
# @return [Hash] Valid Event hash
|
|
45
|
+
# @raise [ArgumentError] If validation fails
|
|
46
|
+
#
|
|
47
|
+
# @example Building an HTTP request event
|
|
48
|
+
# EventBuilder.build(
|
|
49
|
+
# source_type: "http_request",
|
|
50
|
+
# source_data: { method: "POST", path: "/api/users", status_code: 201 },
|
|
51
|
+
# outcome: "success",
|
|
52
|
+
# duration_ms: 124
|
|
53
|
+
# )
|
|
54
|
+
#
|
|
55
|
+
def self.build(
|
|
56
|
+
source_type:,
|
|
57
|
+
source_data:,
|
|
58
|
+
outcome:,
|
|
59
|
+
correlation_id: nil,
|
|
60
|
+
resource_ids: [],
|
|
61
|
+
context: nil,
|
|
62
|
+
duration_ms: nil,
|
|
63
|
+
error_message: nil,
|
|
64
|
+
timestamp: nil
|
|
65
|
+
)
|
|
66
|
+
# Validate and normalize inputs
|
|
67
|
+
validated_source_type = validate_source_type!(source_type)
|
|
68
|
+
validated_source_data = validate_source_data!(source_data)
|
|
69
|
+
validated_outcome = validate_outcome!(outcome)
|
|
70
|
+
validated_timestamp = validate_timestamp!(timestamp)
|
|
71
|
+
validated_resource_ids = validate_resource_ids!(resource_ids)
|
|
72
|
+
validated_context = validate_context!(context)
|
|
73
|
+
validated_duration_ms = validate_duration_ms!(duration_ms)
|
|
74
|
+
|
|
75
|
+
# Validate conditional rules
|
|
76
|
+
validate_outcome_error_consistency!(validated_outcome, error_message)
|
|
77
|
+
|
|
78
|
+
# Sanitize sensitive data
|
|
79
|
+
sanitized_source_data = sanitize_hash(validated_source_data)
|
|
80
|
+
sanitized_context = validated_context.nil? ? nil : sanitize_hash(validated_context)
|
|
81
|
+
|
|
82
|
+
# Build and return Event hash
|
|
83
|
+
{
|
|
84
|
+
timestamp: validated_timestamp,
|
|
85
|
+
source_type: validated_source_type,
|
|
86
|
+
source_data: sanitized_source_data,
|
|
87
|
+
outcome: validated_outcome,
|
|
88
|
+
correlation_id: correlation_id,
|
|
89
|
+
resource_ids: validated_resource_ids,
|
|
90
|
+
context: sanitized_context,
|
|
91
|
+
duration_ms: validated_duration_ms,
|
|
92
|
+
error_message: error_message
|
|
93
|
+
}
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Validates source_type field
|
|
97
|
+
# @param source_type [String, Symbol]
|
|
98
|
+
# @return [String] Validated source type string
|
|
99
|
+
# @raise [ArgumentError]
|
|
100
|
+
def self.validate_source_type!(source_type)
|
|
101
|
+
raise ArgumentError, "source_type is required" if source_type.nil?
|
|
102
|
+
|
|
103
|
+
# Normalize symbol to string
|
|
104
|
+
normalized = source_type.to_s
|
|
105
|
+
|
|
106
|
+
unless VALID_SOURCE_TYPES.include?(normalized)
|
|
107
|
+
raise ArgumentError,
|
|
108
|
+
"source_type must be one of #{VALID_SOURCE_TYPES.join(', ')}, got: #{normalized}"
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
normalized
|
|
112
|
+
end
|
|
113
|
+
private_class_method :validate_source_type!
|
|
114
|
+
|
|
115
|
+
# Validates source_data field
|
|
116
|
+
# @param source_data [Hash]
|
|
117
|
+
# @return [Hash] Validated source data
|
|
118
|
+
# @raise [ArgumentError]
|
|
119
|
+
def self.validate_source_data!(source_data)
|
|
120
|
+
raise ArgumentError, "source_data is required" if source_data.nil?
|
|
121
|
+
raise ArgumentError, "source_data must be a Hash, got: #{source_data.class}" unless source_data.is_a?(Hash)
|
|
122
|
+
|
|
123
|
+
source_data
|
|
124
|
+
end
|
|
125
|
+
private_class_method :validate_source_data!
|
|
126
|
+
|
|
127
|
+
# Validates outcome field
|
|
128
|
+
# @param outcome [String, Symbol]
|
|
129
|
+
# @return [String] Validated outcome string
|
|
130
|
+
# @raise [ArgumentError]
|
|
131
|
+
def self.validate_outcome!(outcome)
|
|
132
|
+
raise ArgumentError, "outcome is required" if outcome.nil?
|
|
133
|
+
|
|
134
|
+
# Normalize symbol to string
|
|
135
|
+
normalized = outcome.to_s
|
|
136
|
+
|
|
137
|
+
unless VALID_OUTCOMES.include?(normalized)
|
|
138
|
+
raise ArgumentError,
|
|
139
|
+
"outcome must be one of #{VALID_OUTCOMES.join(', ')}, got: #{normalized}"
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
normalized
|
|
143
|
+
end
|
|
144
|
+
private_class_method :validate_outcome!
|
|
145
|
+
|
|
146
|
+
# Validates and normalizes timestamp
|
|
147
|
+
# @param timestamp [String, Time, nil]
|
|
148
|
+
# @return [String] ISO 8601 timestamp string
|
|
149
|
+
# @raise [ArgumentError]
|
|
150
|
+
def self.validate_timestamp!(timestamp)
|
|
151
|
+
return Time.now.utc.strftime("%Y-%m-%dT%H:%M:%S.%3NZ") if timestamp.nil?
|
|
152
|
+
|
|
153
|
+
case timestamp
|
|
154
|
+
when String
|
|
155
|
+
# Validate ISO 8601 format by attempting to parse
|
|
156
|
+
begin
|
|
157
|
+
Time.iso8601(timestamp)
|
|
158
|
+
rescue ArgumentError
|
|
159
|
+
raise ArgumentError, "timestamp must be valid ISO 8601 format, got: #{timestamp}"
|
|
160
|
+
end
|
|
161
|
+
timestamp
|
|
162
|
+
when Time
|
|
163
|
+
timestamp.utc.strftime("%Y-%m-%dT%H:%M:%S.%3NZ")
|
|
164
|
+
else
|
|
165
|
+
raise ArgumentError, "timestamp must be a String or Time, got: #{timestamp.class}"
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
private_class_method :validate_timestamp!
|
|
169
|
+
|
|
170
|
+
# Validates resource_ids field
|
|
171
|
+
# @param resource_ids [Array]
|
|
172
|
+
# @return [Array] Validated resource_ids array
|
|
173
|
+
# @raise [ArgumentError]
|
|
174
|
+
def self.validate_resource_ids!(resource_ids)
|
|
175
|
+
raise ArgumentError, "resource_ids must be an Array, got: #{resource_ids.class}" unless resource_ids.is_a?(Array)
|
|
176
|
+
|
|
177
|
+
resource_ids.each_with_index do |resource, index|
|
|
178
|
+
unless resource.is_a?(Hash)
|
|
179
|
+
raise ArgumentError,
|
|
180
|
+
"resource_ids[#{index}] must be a Hash, got: #{resource.class}"
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
unless resource.key?(:resource_type) && resource.key?(:resource_id)
|
|
184
|
+
raise ArgumentError,
|
|
185
|
+
"resource_ids[#{index}] must have :resource_type and :resource_id keys, got: #{resource.keys.inspect}"
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
unless resource[:resource_type].is_a?(String)
|
|
189
|
+
raise ArgumentError,
|
|
190
|
+
"resource_ids[#{index}][:resource_type] must be a String, got: #{resource[:resource_type].class}"
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
unless resource[:resource_id].is_a?(String)
|
|
194
|
+
raise ArgumentError,
|
|
195
|
+
"resource_ids[#{index}][:resource_id] must be a String, got: #{resource[:resource_id].class}"
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
resource_ids
|
|
200
|
+
end
|
|
201
|
+
private_class_method :validate_resource_ids!
|
|
202
|
+
|
|
203
|
+
# Validates context field
|
|
204
|
+
# @param context [Hash, nil]
|
|
205
|
+
# @return [Hash, nil] Validated context
|
|
206
|
+
# @raise [ArgumentError]
|
|
207
|
+
def self.validate_context!(context)
|
|
208
|
+
return nil if context.nil?
|
|
209
|
+
|
|
210
|
+
unless context.is_a?(Hash)
|
|
211
|
+
raise ArgumentError, "context must be a Hash or nil, got: #{context.class}"
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
context
|
|
215
|
+
end
|
|
216
|
+
private_class_method :validate_context!
|
|
217
|
+
|
|
218
|
+
# Validates duration_ms field
|
|
219
|
+
# @param duration_ms [Integer, nil]
|
|
220
|
+
# @return [Integer, nil] Validated duration_ms
|
|
221
|
+
# @raise [ArgumentError]
|
|
222
|
+
def self.validate_duration_ms!(duration_ms)
|
|
223
|
+
return nil if duration_ms.nil?
|
|
224
|
+
|
|
225
|
+
unless duration_ms.is_a?(Integer)
|
|
226
|
+
raise ArgumentError, "duration_ms must be an Integer, got: #{duration_ms.class}"
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
if duration_ms.negative?
|
|
230
|
+
raise ArgumentError, "duration_ms must be >= 0, got: #{duration_ms}"
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
duration_ms
|
|
234
|
+
end
|
|
235
|
+
private_class_method :validate_duration_ms!
|
|
236
|
+
|
|
237
|
+
# Validates outcome and error_message consistency
|
|
238
|
+
# @param outcome [String]
|
|
239
|
+
# @param error_message [String, nil]
|
|
240
|
+
# @raise [ArgumentError]
|
|
241
|
+
def self.validate_outcome_error_consistency!(outcome, error_message)
|
|
242
|
+
if outcome == "failure"
|
|
243
|
+
if error_message.nil? || error_message.empty?
|
|
244
|
+
raise ArgumentError, "error_message is required when outcome is 'failure'"
|
|
245
|
+
end
|
|
246
|
+
elsif outcome == "success"
|
|
247
|
+
unless error_message.nil?
|
|
248
|
+
raise ArgumentError, "error_message must be nil when outcome is 'success'"
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
private_class_method :validate_outcome_error_consistency!
|
|
253
|
+
|
|
254
|
+
# Recursively sanitizes a hash by filtering sensitive keys
|
|
255
|
+
# @param hash [Hash]
|
|
256
|
+
# @return [Hash] Sanitized hash with sensitive values replaced
|
|
257
|
+
def self.sanitize_hash(hash)
|
|
258
|
+
hash.each_with_object({}) do |(key, value), result|
|
|
259
|
+
if sensitive_key?(key)
|
|
260
|
+
result[key] = "[FILTERED]"
|
|
261
|
+
elsif value.is_a?(Hash)
|
|
262
|
+
result[key] = sanitize_hash(value)
|
|
263
|
+
elsif value.is_a?(Array)
|
|
264
|
+
result[key] = value.map { |item| item.is_a?(Hash) ? sanitize_hash(item) : item }
|
|
265
|
+
else
|
|
266
|
+
result[key] = value
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
private_class_method :sanitize_hash
|
|
271
|
+
|
|
272
|
+
# Checks if a key is sensitive (case-insensitive substring match)
|
|
273
|
+
# @param key [String, Symbol]
|
|
274
|
+
# @return [Boolean]
|
|
275
|
+
def self.sensitive_key?(key)
|
|
276
|
+
key_str = key.to_s.downcase
|
|
277
|
+
SENSITIVE_KEYS.any? { |sensitive| key_str.include?(sensitive) }
|
|
278
|
+
end
|
|
279
|
+
private_class_method :sensitive_key?
|
|
280
|
+
end
|
|
281
|
+
end
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module EzLogsAgent
|
|
4
|
+
# FlushScheduler orchestrates periodic flushing of buffered events to the server.
|
|
5
|
+
#
|
|
6
|
+
# Responsibilities:
|
|
7
|
+
# - Start a single background thread that wakes up every `send_interval` seconds
|
|
8
|
+
# - Flush Buffer and send events via RetrySender
|
|
9
|
+
# - Shut down cleanly when requested
|
|
10
|
+
#
|
|
11
|
+
# Non-responsibilities:
|
|
12
|
+
# - Does NOT retry (RetrySender handles that)
|
|
13
|
+
# - Does NOT interpret Transport results
|
|
14
|
+
# - Does NOT manage Rails lifecycle (Railtie handles that)
|
|
15
|
+
# - Does NOT mutate or inspect events
|
|
16
|
+
class FlushScheduler
|
|
17
|
+
class << self
|
|
18
|
+
# Start the background flush thread
|
|
19
|
+
# Idempotent: safe to call multiple times
|
|
20
|
+
def start
|
|
21
|
+
return if running?
|
|
22
|
+
|
|
23
|
+
@stop_requested = false
|
|
24
|
+
@thread = Thread.new { run_loop }
|
|
25
|
+
|
|
26
|
+
Logger.debug("[FlushScheduler] Started")
|
|
27
|
+
rescue => error
|
|
28
|
+
Logger.error("[FlushScheduler] Failed to start: #{error.class} - #{error.message}")
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Stop the background flush thread
|
|
32
|
+
# Blocks until thread exits
|
|
33
|
+
# Idempotent: safe to call multiple times
|
|
34
|
+
def stop
|
|
35
|
+
return unless running?
|
|
36
|
+
|
|
37
|
+
@stop_requested = true
|
|
38
|
+
@thread.join if @thread
|
|
39
|
+
|
|
40
|
+
Logger.debug("[FlushScheduler] Stopped")
|
|
41
|
+
rescue => error
|
|
42
|
+
Logger.error("[FlushScheduler] Failed to stop: #{error.class} - #{error.message}")
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Check if the scheduler is currently running
|
|
46
|
+
def running?
|
|
47
|
+
@thread&.alive? == true
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
# Background loop: sleep → flush → send → repeat
|
|
53
|
+
def run_loop
|
|
54
|
+
loop do
|
|
55
|
+
break if @stop_requested
|
|
56
|
+
|
|
57
|
+
sleep_interval
|
|
58
|
+
break if @stop_requested
|
|
59
|
+
|
|
60
|
+
flush_and_send
|
|
61
|
+
end
|
|
62
|
+
rescue => error
|
|
63
|
+
Logger.error("[FlushScheduler] Loop crashed: #{error.class} - #{error.message}")
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Sleep for configured interval
|
|
67
|
+
def sleep_interval
|
|
68
|
+
interval = send_interval
|
|
69
|
+
sleep(interval)
|
|
70
|
+
rescue => error
|
|
71
|
+
Logger.error("[FlushScheduler] Sleep failed: #{error.message}")
|
|
72
|
+
sleep(5) # Defensive fallback
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Flush buffer and send events
|
|
76
|
+
def flush_and_send
|
|
77
|
+
events = Buffer.flush
|
|
78
|
+
return if events.nil? || events.empty?
|
|
79
|
+
|
|
80
|
+
RetrySender.send(events)
|
|
81
|
+
rescue => error
|
|
82
|
+
Logger.error("[FlushScheduler] Flush and send failed: #{error.class} - #{error.message}")
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Get send_interval from configuration with defensive fallback
|
|
86
|
+
def send_interval
|
|
87
|
+
interval = EzLogsAgent.configuration.send_interval
|
|
88
|
+
return 5 if interval.nil?
|
|
89
|
+
return 5 if !interval.is_a?(Numeric)
|
|
90
|
+
return 5 if interval <= 0
|
|
91
|
+
|
|
92
|
+
interval
|
|
93
|
+
rescue => error
|
|
94
|
+
Logger.error("[FlushScheduler] Failed to read send_interval: #{error.message}")
|
|
95
|
+
5
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module EzLogsAgent
|
|
4
|
+
# Minimal, defensive logging utility for the gem.
|
|
5
|
+
# Delegates to Rails.logger when available, falls back to STDERR.
|
|
6
|
+
# Never raises exceptions, never crashes the host application.
|
|
7
|
+
module Logger
|
|
8
|
+
LOG_LEVELS = {
|
|
9
|
+
debug: 0,
|
|
10
|
+
info: 1,
|
|
11
|
+
warn: 2,
|
|
12
|
+
error: 3
|
|
13
|
+
}.freeze
|
|
14
|
+
|
|
15
|
+
class << self
|
|
16
|
+
def debug(message)
|
|
17
|
+
log(:debug, message)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def info(message)
|
|
21
|
+
log(:info, message)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def warn(message)
|
|
25
|
+
log(:warn, message)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def error(message)
|
|
29
|
+
log(:error, message)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def log(level, message)
|
|
35
|
+
# Early return if this log level should not be logged
|
|
36
|
+
return unless should_log?(level)
|
|
37
|
+
|
|
38
|
+
prefixed_message = "[EzLogsAgent] #{message}"
|
|
39
|
+
|
|
40
|
+
if defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger
|
|
41
|
+
Rails.logger.public_send(level, prefixed_message)
|
|
42
|
+
else
|
|
43
|
+
# Fallback to stderr when Rails.logger is not available
|
|
44
|
+
# Note: should_log? already checked above, so this respects log_level
|
|
45
|
+
$stderr.puts("[#{level.upcase}] #{prefixed_message}")
|
|
46
|
+
end
|
|
47
|
+
rescue => e
|
|
48
|
+
# Defensive: logging must never crash the host application.
|
|
49
|
+
# Silently swallow any logging errors.
|
|
50
|
+
nil
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def should_log?(level)
|
|
54
|
+
configured_level = EzLogsAgent.configuration.log_level
|
|
55
|
+
LOG_LEVELS[level] >= LOG_LEVELS[configured_level]
|
|
56
|
+
rescue => e
|
|
57
|
+
# If we can't determine log level, allow logging (fail open).
|
|
58
|
+
true
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|