-
Notifications
You must be signed in to change notification settings - Fork 0
/
Log4Xojo.xojo_code
569 lines (474 loc) · 18 KB
/
Log4Xojo.xojo_code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
#tag Class
Protected Class Log4Xojo
#tag Method, Flags = &h0
Sub Constructor(Name As String)
If Name.Trim = "" Then
Raise New InvalidArgumentException("Log name cannot be empty.")
End If
// Set the log name
mLogName = Name
// Initialize log level
mCurrentLogLevel = LogLevel.Debug
// Initialize default log destinations
mLogDestinations.Add(LogDestination.DebugLog)
// Set the log file path using the log name and current date
mLogFilePath = SpecialFolder.Documents.Child(GenerateLogFileName()).NativePath
// Initialize the Mutex with the log name to ensure uniqueness
mLogQueueMutex = New Mutex("Log4Xojo_" + Name)
// Set up a background thread for logging
mLogThread = New Thread
AddHandler mLogThread.Run, AddressOf LogThreadHandler
mRunning = True
mLogThread.Start
End Sub
#tag EndMethod
#tag Method, Flags = &h0
Sub Destructor()
// // Wait for remaining messages with a timeout
// Var startTime As Double = System.Microseconds
// Const TimeoutMicroseconds = 5000000 // 5 seconds
//
// While mLogQueue.Count > 0
// mLogThread.SleepCurrent(mThreadSleepDuration)
//
// // Prevent infinite wait
// If System.Microseconds - startTime > TimeoutMicroseconds Then
// System.DebugLog("Log4Xojo: Timeout waiting for log queue to clear")
// Exit
// End If
// Wend
//
// // Stop the thread
// If mLogThread <> Nil Then
// mLogThread.Stop
// End If
StopLogging
End Sub
#tag EndMethod
#tag Method, Flags = &h21
Private Function GenerateLogFileName() As String
Var currentDate As String = DateTime.Now.SQLDate // Get the current date in YYYY-MM-DD format
Return mLogName + "_" + currentDate + ".txt"
End Function
#tag EndMethod
#tag Method, Flags = &h0
Sub Log(message As String, level As LogLevel, Optional location As String = "")
// Check if the current message level is high enough to be logged
If Integer(level) < Integer(mCurrentLogLevel) Then
Return // Don't log messages below the current log level
End If
// Prepare the log message with timestamp and log level
Var formattedMessage As String
Var timestamp As String
timestamp = DateTime.Now.SQLDateTime
Var l As String = StringValue(level)
// Construct the message with optional location
If location = "" Then
formattedMessage = "[" + timestamp + "] [" + l + "] " + message
Else
formattedMessage = "[" + timestamp + "] [" + location + "] [" + l + "] " + message
End If
// Existing logging logic remains the same
For Each destination As LogDestination In mLogDestinations
Select Case destination
Case LogDestination.DebugLog
System.DebugLog(formattedMessage)
Case LogDestination.SystemLog
System.Log(SystemLogLevelFromLogLevel(level), formattedMessage)
Case LogDestination.FileLog
// Use mutex when adding to queue
mLogQueueMutex.Enter
Try
mLogQueue.Add(formattedMessage)
Finally
mLogQueueMutex.Leave
End Try
Case LogDestination.All
System.DebugLog(formattedMessage)
System.Log(SystemLogLevelFromLogLevel(level), formattedMessage)
// Use mutex when adding to queue
// Optional: Prevent queue from growing too large
mLogQueueMutex.Enter
Try
If mLogQueue.Count >= MaxQueueSize Then
// Optionally: Remove oldest message to make room
mLogQueue.RemoveAt(0)
End If
// Add new message
mLogQueue.Add(formattedMessage)
Finally
mLogQueueMutex.Leave
End Try
End Select
Next
End Sub
#tag EndMethod
#tag Method, Flags = &h21
Private Sub LogThreadHandler(sender As Thread)
While mRunning Or mLogQueue.Count > 0
Try
// Protect queue access
mLogQueueMutex.Enter
Var messagesToLog() As String
// Retrieve multiple messages from the queue (batch processing)
For i As Integer = 0 To 49 // Process up to 50 messages at a time
If mLogQueue.Count > 0 Then
messagesToLog.Add(mLogQueue(0))
mLogQueue.RemoveAt(0)
Else
Exit
End If
Next
mLogQueueMutex.Leave
// Write messages outside mutex to reduce lock time
If messagesToLog.Count > 0 Then
WriteToFile(messagesToLog)
End If
// Sleep to prevent busy waiting
If messagesToLog.Count = 0 Then
sender.SleepCurrent(mThreadSleepDuration)
End If
Catch e As RuntimeException
// Log any unexpected errors
System.DebugLog("Log4Xojo: Logging thread error - " + e.Message)
// Prevent tight error loops
sender.SleepCurrent(mThreadSleepDuration)
End Try
Wend
End Sub
#tag EndMethod
#tag Method, Flags = &h21
Private Sub RotateLogFile(currentLogFile As FolderItem)
Try
// Get the folder where the log file is stored
Var folder As FolderItem = currentLogFile.Parent
If folder = Nil Then
System.DebugLog("Log4Xojo: Log file folder does not exist.")
Return
End If
// Get the base name and extension using GenerateLogFileName
Var baseName As String = mLogName + "_" + DateTime.Now.SQLDate // Same logic as GenerateLogFileName
Var extension As String = currentLogFile.Extension
If extension.Trim = "" Then
extension = "txt" // Default to .txt if no extension
End If
// If we've reached the maximum number of backup files, delete the oldest one
Var oldestBackupFile As FolderItem = folder.Child(baseName + "_" + Str(mMaxBackupFiles) + "." + extension)
If oldestBackupFile.Exists Then
oldestBackupFile.Remove
End If
// Shift existing backup files
For i As Integer = mMaxBackupFiles - 1 DownTo 1
Var oldLogFile As FolderItem = folder.Child(baseName + "_" + Str(i) + "." + extension)
Var newLogFile As FolderItem = folder.Child(baseName + "_" + Str(i+1) + "." + extension)
If oldLogFile.Exists Then
oldLogFile.Name = newLogFile.Name
End If
Next
// Rename the current log file to the first backup
Var firstBackupFile As FolderItem = folder.Child(baseName + "_1." + extension)
currentLogFile.Name = firstBackupFile.Name
// Create a new log file
Var newLogFile As New FolderItem(folder.Child(GenerateLogFileName()).NativePath, FolderItem.PathModes.Native)
Var out As TextOutputStream = TextOutputStream.Create(newLogFile)
out.Close
Catch e As IOException
// Handle file rotation errors
System.DebugLog("Log4Xojo: Unable to rotate log file - " + e.Message)
End Try
End Sub
#tag EndMethod
#tag Method, Flags = &h0
Sub SetLogDestinations(ParamArray destinations As LogDestination)
// Clear existing destinations
mLogDestinations.RemoveAll
// Add the provided destinations
For Each destination As LogDestination In destinations
mLogDestinations.Add(destination)
Next
End Sub
#tag EndMethod
#tag Method, Flags = &h0
Sub SetLogFilePath(baseLocation As FolderItem)
If baseLocation = Nil Or Not baseLocation.Exists Then
Raise New InvalidArgumentException("Base location is invalid or does not exist.")
End If
// Ensure the folder is writable
If Not baseLocation.IsWriteable Then
Raise New InvalidArgumentException("Base location is not writable.")
End If
// Append the log name and current date to the base location to create the full file path
mLogFilePath = baseLocation.Child(GenerateLogFileName()).NativePath
End Sub
#tag EndMethod
#tag Method, Flags = &h0
Sub SetLogLevel(level As LogLevel)
mCurrentLogLevel = level
End Sub
#tag EndMethod
#tag Method, Flags = &h0
Sub SetMaxBackupFiles(max As Integer)
mMaxBackupFiles = max
End Sub
#tag EndMethod
#tag Method, Flags = &h0
Sub SetMaxLogFileSize(sizeInBytes As Integer)
mMaxLogFileSize = sizeInBytes
End Sub
#tag EndMethod
#tag Method, Flags = &h21
Private Sub SetThreadSleepDuration(duration As Integer)
mThreadSleepDuration = duration
End Sub
#tag EndMethod
#tag Method, Flags = &h0
Sub StopLogging()
// Allow the thread to continue processing the queue
If mLogThread <> Nil Then
Var startTime As Double = System.Microseconds
Const TimeoutMicroseconds = 5000000 // 5 seconds
While mLogQueue.Count > 0 And System.Microseconds - startTime < TimeoutMicroseconds
// Sleep to allow the thread to process the queue
mLogThread.SleepCurrent(mThreadSleepDuration)
Wend
// If the queue is still not empty, log a warning
If mLogQueue.Count > 0 Then
System.DebugLog("Log4Xojo: Timeout waiting for log queue to clear")
End If
End If
// Now signal the thread to stop
mRunning = False
End Sub
#tag EndMethod
#tag Method, Flags = &h21
Private Function StringValue(e As LogLevel) As String
Select Case e
Case LogLevel.Debug
Return "DEBUG"
Case LogLevel.Info
Return "INFO"
Case LogLevel.Warning
Return "WARNING"
Case LogLevel.Error
Return "ERROR"
Case LogLevel.Critical
Return "CRITICAL"
Else
Return "UNKNOWN"
End Select
End Function
#tag EndMethod
#tag Method, Flags = &h21
Private Function SystemLogLevelFromLogLevel(level As LogLevel) As Integer
Select Case level
Case LogLevel.Debug
Return System.LogLevelDebug
Case LogLevel.Info
Return System.LogLevelInformation
Case LogLevel.Warning
Return System.LogLevelWarning
Case LogLevel.Error
Return System.LogLevelError
Case LogLevel.Critical
Return System.LogLevelCritical
Else
Return System.LogLevelInformation
End Select
End Function
#tag EndMethod
#tag Method, Flags = &h21
Private Sub WriteToFile(messages() As String)
If mLogFilePath = "" Then
mLogFilePath = SpecialFolder.Documents.Child(GenerateLogFileName()).NativePath
End If
Try
// Create a FolderItem from the path
Var logFile As New FolderItem(mLogFilePath, FolderItem.PathModes.Native)
// Check and manage file size if needed
If logFile.Exists And mMaxLogFileSize > 0 Then
If logFile.Length >= mMaxLogFileSize Then
RotateLogFile(logFile)
End If
End If
// Open the file for appending
Var out As TextOutputStream
out = TextOutputStream.Open(logFile)
// Write all messages
For Each message As String In messages
out.WriteLine(message)
Next
// Close the file
out.Close
Catch e As IOException
// Add the messages back to the queue for retry
mLogQueueMutex.Enter
Try
For Each message As String In messages
mLogQueue.Add(message)
Next
Finally
mLogQueueMutex.Leave
End Try
System.DebugLog("Log4Xojo: Unable to write to log file - " + e.Message)
Catch e As RuntimeException
System.DebugLog("Log4Xojo: Unexpected error writing log file - " + e.Message)
End Try
End Sub
#tag EndMethod
#tag Note, Name = How to use Log4Xojo
Official GitHub repo: https://github.com/xojo/log4xojo
Blog: https://blog.xojo.com/2024/11/26/log4xojo-a-more-powerful-way-to-manage-your-app-logging/
# 1. Monitoring Application Performance in Production
In a production environment, it’s essential to keep track of your application’s behavior without impacting performance. With Log4Xojo, you can:
Log important application events (e.g., user activity, API calls).
Use file logging to store these logs persistently for later analysis.
Filter logs by severity to avoid unnecessary noise (e.g., only warnings, errors, and critical issues).
Example:
Var l4x As New Log4Xojo("ProductionLog")
l4x.SetLogDestinations(Log4Xojo.LogDestination.FileLog)
l4x.SetLogLevel(Log4Xojo.LogLevel.Warning)
// Log application events
l4x.Log("User logged in", Log4Xojo.LogLevel.Info) // Ignored (below warning level)
l4x.Log("Database connection failed", Log4Xojo.LogLevel.Error) // Logged
l4x.Log("Critical: Payment gateway unreachable", Log4Xojo.LogLevel.Critical) // Logged
Outcome: Only warnings, errors, and critical messages are logged to a file for postmortem analysis without overwhelming the log with lower-priority messages.
# 2. Creating a Diagnostic Tool for End Users
When troubleshooting issues reported by end users, having detailed logs can be invaluable. With Log4Xojo, you can:
Log messages to a file on the user’s machine.
Include optional location tags to pinpoint where issues occur in your code.
Use log rotation to prevent log files from consuming too much disk space.
Example:
Var l4x As New Log4Xojo("UserDiagnostics")
l4x.SetLogDestinations(Log4Xojo.LogDestination.FileLog)
l4x.SetLogFilePath(SpecialFolder.Documents)
l4x.SetMaxLogFileSize(1 * 1024 * 1024) // 1 MB
l4x.SetMaxBackupFiles(3)
// Log diagnostic information
l4x.Log("Application launched", Log4Xojo.LogLevel.Info, CurrentMethodName)
l4x.Log("Error: File not found", Log4Xojo.LogLevel.Error, "FileManager.LoadFile")
l4x.Log("User clicked 'Submit'", Log4Xojo.LogLevel.Debug, "MainWindow.HandleSubmit")
Outcome: You can ask users to send the log files stored in their Documents folder for review, helping you quickly diagnose and fix issues.
# 3. Tracking User Activity in Enterprise Applications
In enterprise applications, logging user activity is often a requirement for auditing or compliance purposes. With Log4Xojo, you can:
Use multi-destination logging to send activity logs to both the system logs and a central log file.
Include relevant context for each log entry (e.g., user ID, method).
Example:
Var l4x As New Log4Xojo("AuditLog")
l4x.SetLogDestinations(Log4Xojo.LogDestination.SystemLog, Log4Xojo.LogDestination.FileLog)
l4x.SetLogFilePath(SpecialFolder.Documents)
// Log user activity
Var userID As String = "User123"
l4x.Log(userID + " logged in", Log4Xojo.LogLevel.Info, "AuthManager.Login")
l4x.Log(userID + " updated profile", Log4Xojo.LogLevel.Info, "ProfileManager.UpdateProfile")
l4x.Log(userID + " attempted unauthorized access", Log4Xojo.LogLevel.Warning, "SecurityManager.CheckPermissions")
Outcome: Both system logs and a persistent file log are updated with the user’s activities, ensuring compliance and easy traceability.
# 4. Handling Errors and Crashes Gracefully
When an application crashes, logs are often the only way to understand what went wrong. Log4Xojo can:
Capture error and critical logs leading up to a crash.
Rotate logs to avoid losing older, relevant logs.
Save logs to a file for recovery after a crash.
Example:
Var l4x As New Log4Xojo("CrashLogs")
l4x.SetLogDestinations(Log4Xojo.LogDestination.FileLog)
l4x.SetMaxBackupFiles(5)
l4x.SetMaxLogFileSize(1 * 1024 * 1024) // 1 MB
Try
// Simulate application logic
Raise New RuntimeException("Simulated crash")
Catch e As RuntimeException
l4x.Log("Critical error: " + e.Message, Log4Xojo.LogLevel.Critical, CurrentMethodName)
End Try
Outcome: The log files can be used to investigate the cause of the crash.
#tag EndNote
#tag Property, Flags = &h21
Private mCurrentLogLevel As LogLevel
#tag EndProperty
#tag Property, Flags = &h21
Private mLogDestinations() As LogDestination
#tag EndProperty
#tag Property, Flags = &h21
Private mLogFilePath As String
#tag EndProperty
#tag Property, Flags = &h21
Private mLogName As String
#tag EndProperty
#tag Property, Flags = &h21
Private mLogQueue() As String
#tag EndProperty
#tag Property, Flags = &h21
Private mLogQueueMutex As Mutex
#tag EndProperty
#tag Property, Flags = &h21
Private mLogThread As Thread
#tag EndProperty
#tag Property, Flags = &h21
Private mMaxBackupFiles As Integer = 10
#tag EndProperty
#tag Property, Flags = &h21
Private mMaxLogFileSize As Integer
#tag EndProperty
#tag Property, Flags = &h21
Private mRunning As Boolean
#tag EndProperty
#tag Property, Flags = &h21
Private mThreadSleepDuration As Integer = 100
#tag EndProperty
#tag Constant, Name = MaxQueueSize, Type = Double, Dynamic = False, Default = \"10000", Scope = Public, Description = 4D6178696D756D206D6573736167657320696E20746865207175657565
#tag EndConstant
#tag Enum, Name = LogDestination, Type = Integer, Flags = &h0
DebugLog
SystemLog
FileLog
All
#tag EndEnum
#tag Enum, Name = LogLevel, Type = Integer, Flags = &h0
Debug
Info
Warning
Error
Critical
#tag EndEnum
#tag ViewBehavior
#tag ViewProperty
Name="Name"
Visible=true
Group="ID"
InitialValue=""
Type="String"
EditorType=""
#tag EndViewProperty
#tag ViewProperty
Name="Index"
Visible=true
Group="ID"
InitialValue="-2147483648"
Type="Integer"
EditorType=""
#tag EndViewProperty
#tag ViewProperty
Name="Super"
Visible=true
Group="ID"
InitialValue=""
Type="String"
EditorType=""
#tag EndViewProperty
#tag ViewProperty
Name="Left"
Visible=true
Group="Position"
InitialValue="0"
Type="Integer"
EditorType=""
#tag EndViewProperty
#tag ViewProperty
Name="Top"
Visible=true
Group="Position"
InitialValue="0"
Type="Integer"
EditorType=""
#tag EndViewProperty
#tag EndViewBehavior
End Class
#tag EndClass