GPAL - Generally Positive Automation Library v1.0
GPAL The Fluent Automation LIbrary
Loading...
Searching...
No Matches
GPALLogger.cs
1// =============================================================================
2// GPAL - Generally Positive Automation Library
3// Copyright © 2026 Software Decisions, Inc. All rights reserved.
4//
5// This file is part of GPAL.
6// Licensed under the Business Source License 1.1
7//
8// Primary development, architecture, and vision by Michael B. Vederman,
9// CEO of Software Decisions, Inc., Texas.
10//
11// Internal development maintained privately.
12// Public releases appear on GitHub: https://github.com/SoftwareDecisionsInc/GPAL.
13//
14// See LICENSE for full terms, including Additional Use Grant.
15// =============================================================================
16
17using System;
18using System.Collections.Generic;
19using System.Diagnostics;
20using System.IO;
21using System.Linq;
22using System.Text;
23using System.Threading.Tasks;
24using static GenerallyPositive.Enums;
25
26namespace GenerallyPositive
27{
28
46 public class GPALLogger : IGPALLogger
47 {
49 private string RootDirectory { get; set; }
50 private DirectoryStructure DirectoryStructure { get; set; }
51 private string Filename { get; set; }
52 private FilenamePattern FilenamePattern { get; set; }
53 private string Delimiter { get; set; }
54 private GPALDatabase Database { get; set; }
55 private LogDateFormat LogDateFormat { get; set; } = LogDateFormat.NOTSET;
56
61 public string LogDirectory { get; private set; }
66 public string LogFilename { get; private set; }
71 public string LogFullPath { get; private set; }
72
73 private bool noTargetReported = false;
74
75 // one per logger, not one for all of them. writing publishes events of its own, and with a logger
76 // attached to the publish path that lands in another logger's Log - which took this same converter,
77 // reset its input and output, and left the write that was already in progress to save the wrong thing
78 private GPALConverter converter = new GPALConverter();
79 private GPALEventType LogEventTypes { get; set; } = GPALEventType.All;
80
86 {
87 return this;
88 }
89
93 internal GPALLogger()
94 {
95 IGPALConverter converter = GPAL.Converter.ToGPALObject();
96 // Default values
97 RootDirectory = Directory.GetCurrentDirectory();
98 DirectoryStructure = DirectoryStructure.None;
99 FilenamePattern = FilenamePattern.None;
100 }
109 public IAllowLoggingSettings WithLogEventTypes(GPALEventType logEventTypes)
110 {
111 LogEventTypes = logEventTypes;
112 return this;
113 }
114
122 {
123 Database = database;
124 return this;
125 }
126
133 {
134 RootDirectory = path;
135 return this;
136 }
137
144 public IAllowLoggingSettings WithDirectoryStructure(DirectoryStructure type)
145 {
146 DirectoryStructure = type;
147 return this;
148 }
149
157 {
158 Filename = name;
159 return this;
160 }
161
168 public IAllowLoggingSettings WithFilenamePattern(FilenamePattern pattern)
169 {
170 FilenamePattern = pattern;
171 return this;
172 }
173
180 {
181 Delimiter = delimiter.ToString();
182 converter.ConverterSettings.InDelimiter = delimiter;
183 return this;
184 }
185
191 public IAllowLoggingSettings WithLoggingFormat(LogDateFormat logDateFormat)
192 {
193 LogDateFormat = logDateFormat;
194 return this;
195 }
196
202 private string GetLogFilePath()
203 {
204 string logDirectory = FileHelper.EnsureDirectoryEndsWithBackslash(RootDirectory);
205 string year = DateTime.Now.ToString("yyyy");
206 string month = DateTime.Now.ToString("MM");
207 string day = DateTime.Now.ToString("dd");
208 string hr = DateTime.Now.ToString("HH");
209 string julian = DateTime.Now.DayOfYear.ToString("000");
210
211
212 // Create directory if it doesn't exist
213 Directory.CreateDirectory(logDirectory);
214
215 // Adjust directory structure based on the selected type
216 switch (DirectoryStructure)
217 {
218 case DirectoryStructure.YM:
219 // Create year and month directories
220 logDirectory = Path.Combine(logDirectory, year);
221 Directory.CreateDirectory(logDirectory);
222
223 logDirectory = Path.Combine(logDirectory, month);
224 Directory.CreateDirectory(logDirectory);
225
226 break;
227 case DirectoryStructure.YMD:
228 // Create year, month, and day directories
229 logDirectory = Path.Combine(logDirectory, year);
230 Directory.CreateDirectory(logDirectory);
231
232 logDirectory = Path.Combine(logDirectory, month);
233 Directory.CreateDirectory(logDirectory);
234
235 logDirectory = Path.Combine(logDirectory, day);
236 Directory.CreateDirectory(logDirectory);
237 break;
238 case DirectoryStructure.YMDH:
239 // Create year, month, day, and hour directories
240 logDirectory = Path.Combine(logDirectory, year);
241 Directory.CreateDirectory(logDirectory);
242
243 logDirectory = Path.Combine(logDirectory, month);
244 Directory.CreateDirectory(logDirectory);
245
246 logDirectory = Path.Combine(logDirectory, day);
247 Directory.CreateDirectory(logDirectory);
248
249 logDirectory = Path.Combine(logDirectory, hr);
250 Directory.CreateDirectory(logDirectory);
251 break;
252 case DirectoryStructure.MDH:
253 logDirectory = Path.Combine(logDirectory, month);
254 Directory.CreateDirectory(logDirectory);
255
256 logDirectory = Path.Combine(logDirectory, day);
257 Directory.CreateDirectory(logDirectory);
258
259 logDirectory = Path.Combine(logDirectory, hr);
260 Directory.CreateDirectory(logDirectory);
261 break;
262 case DirectoryStructure.MD:
263 logDirectory = Path.Combine(logDirectory, month);
264 Directory.CreateDirectory(logDirectory);
265
266 logDirectory = Path.Combine(logDirectory, day);
267 Directory.CreateDirectory(logDirectory);
268 break;
269 case DirectoryStructure.M:
270 logDirectory = Path.Combine(logDirectory, month);
271 Directory.CreateDirectory(logDirectory);
272 break;
273 case DirectoryStructure.DH:
274 logDirectory = Path.Combine(logDirectory, day);
275 Directory.CreateDirectory(logDirectory);
276
277 logDirectory = Path.Combine(logDirectory, hr);
278 Directory.CreateDirectory(logDirectory);
279 break;
280 case DirectoryStructure.D:
281 logDirectory = Path.Combine(logDirectory, day);
282 Directory.CreateDirectory(logDirectory);
283 break;
284 case DirectoryStructure.H:
285 logDirectory = Path.Combine(logDirectory, hr);
286 Directory.CreateDirectory(logDirectory);
287 break;
288 case DirectoryStructure.Julian:
289 logDirectory = Path.Combine(logDirectory, julian);
290 Directory.CreateDirectory(logDirectory);
291 break;
292 case DirectoryStructure.None:
293 break;
294 default:
295 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Unable to figure out directory structure [{DirectoryStructure}]", this, GPALObjectType.Logger);
296 break;
297 }
298
299 // Generate filename based on pattern
300 string timestamp = string.Empty;
301 string fileExtension = Filename.Split('.').Last();
302 string filenameWithoutExtension = Filename.Substring(0, Filename.LastIndexOf('.'));
303
304 switch (FilenamePattern)
305 {
306 case FilenamePattern.YMDHMSm:
307 timestamp = DateTime.Now.ToString("yyyyMMddHHmmssfff");
308 break;
309 case FilenamePattern.YMDHMS:
310 timestamp = DateTime.Now.ToString("yyyyMMddHHmmss");
311 break;
312 case FilenamePattern.YMDHM:
313 timestamp = DateTime.Now.ToString("yyyyMMddHHmm");
314 break;
315 case FilenamePattern.YMDH:
316 timestamp = DateTime.Now.ToString("yyyyMMddHH");
317 break;
318 case FilenamePattern.YMD:
319 timestamp = DateTime.Now.ToString("yyyyMMdd");
320 break;
321 case FilenamePattern.YM:
322 timestamp = DateTime.Now.ToString("yyyyMM");
323 break;
324 case FilenamePattern.MM:
325 timestamp = DateTime.Now.ToString("MM");
326 break;
327 case FilenamePattern.Y:
328 timestamp = DateTime.Now.ToString("yyyy");
329 break;
330 case FilenamePattern.MDHMSm:
331 timestamp = DateTime.Now.ToString("MMddHHmmssfff");
332 break;
333 case FilenamePattern.MDHMS:
334 timestamp = DateTime.Now.ToString("MMddHHmmss");
335 break;
336 case FilenamePattern.MDHM:
337 timestamp = DateTime.Now.ToString("MMddHHmm");
338 break;
339 case FilenamePattern.MDH:
340 timestamp = DateTime.Now.ToString("MMddHH");
341 break;
342 case FilenamePattern.DHMSm:
343 timestamp = DateTime.Now.ToString("ddHHmmssfff");
344 break;
345 case FilenamePattern.DHMS:
346 timestamp = DateTime.Now.ToString("ddHHmmss");
347 break;
348 case FilenamePattern.DHM:
349 timestamp = DateTime.Now.ToString("ddHHmm");
350 break;
351 case FilenamePattern.DH:
352 timestamp = DateTime.Now.ToString("ddHH");
353 break;
354 case FilenamePattern.D:
355 timestamp = DateTime.Now.Day.ToString();
356 break;
357 case FilenamePattern.HMSm:
358 timestamp = DateTime.Now.ToString("HHmmssfff");
359 break;
360 case FilenamePattern.HMS:
361 timestamp = DateTime.Now.ToString("HHmmss");
362 break;
363 case FilenamePattern.HM:
364 timestamp = DateTime.Now.ToString("HHmm");
365 break;
366 case FilenamePattern.H:
367 timestamp = DateTime.Now.ToString("HH");
368 break;
369 case FilenamePattern.MSm:
370 timestamp = DateTime.Now.ToString("mmssfff");
371 break;
372 case FilenamePattern.MS:
373 timestamp = DateTime.Now.ToString("mmss");
374 break;
375 case FilenamePattern.M:
376 timestamp = DateTime.Now.ToString("mm");
377 break;
378 case FilenamePattern.Sm:
379 timestamp = DateTime.Now.ToString("ssfff");
380 break;
381 case FilenamePattern.S:
382 timestamp = DateTime.Now.ToString("ss");
383 break;
384 case FilenamePattern.m:
385 timestamp = DateTime.Now.ToString("fff");
386 break;
387 case FilenamePattern.Julian:
388 timestamp = julian;
389 break;
390 case FilenamePattern.None:
391 return Path.Combine(logDirectory, Filename);
392 default:
393 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Unable to determine filename patter [{FilenamePattern}]. Using [{Filename}].", this, GPALObjectType.Logger);
394 return Path.Combine(logDirectory, Filename);
395 }
396
397 if (!string.IsNullOrEmpty(timestamp))
398 filenameWithoutExtension = $"{filenameWithoutExtension}_{timestamp}";
399
400 return Path.Combine(logDirectory, $"{filenameWithoutExtension}.{fileExtension}");
401 }
408 public static string GetFormatString(LogDateFormat format)
409 {
410 switch (format)
411 {
412 case LogDateFormat.YMD_HMS:
413 return "yyyy-MM-dd HH:mm:ss";
414 case LogDateFormat.YMD_HMS_FFF:
415 return "yyyy-MM-dd HH:mm:ss.fff";
416 case LogDateFormat.DMY_HMS:
417 return "dd-MM-yyyy HH:mm:ss";
418 case LogDateFormat.MDY_HMS:
419 return "MM-dd-yyyy HH:mm:ss";
420 case LogDateFormat.DMY:
421 return "dd-MM-yyyy";
422 case LogDateFormat.MDY:
423 return "MM-dd-yyyy";
424 case LogDateFormat.YMD:
425 return "yyyy-MM-dd";
426 case LogDateFormat.HMS:
427 return "HH:mm:ss";
428 case LogDateFormat.HM:
429 return "HH:mm";
430 case LogDateFormat.MDY_Long:
431 return "MMMM dd, yyyy";
432 default:
433 throw new ArgumentException("Unknown LogDateFormat value");
434 }
435 }
436
453 // one writer at a time. every entry arrives on whatever thread published it, and GPAL.Workflow and
454 // the puppeteer receive loop both publish while a workflow is running. AppendAllText opens the file
455 // with FileShare.Read, so a second thread writing at the same moment is refused and that entry is
456 // lost. static rather than per instance, because two loggers pointed at one filename collide too
457 private static readonly object writeLock = new object();
458
461 internal IAllowLoggingSettings Log(string logThis, string raisedBy, LogType entryType)
462 {
463 lock (writeLock)
464 WriteEntry(logThis, raisedBy, entryType);
465
466 return this;
467 }
468
469 private void WriteEntry(string logThis, string raisedBy, LogType entryType)
470 {
471 // the names are kept the same on both enums so this maps without a table. an entry whose type is not
472 // in the set is not written
473 if (false == Enum.TryParse(entryType.ToString(), out GPALEventType logEventType) || false == LogEventTypes.HasFlag(logEventType))
474 return;
475
476 DateTime currentDate = DateTime.Now;
477 string formattedDate;
478 string logFilePath = null;
479
480 // whoever raised the event, which the publisher already worked out. the stack only knows who called
481 // Log, and that is always the publisher now, so every entry would say PublishSimpleEvent
482 string name = raisedBy;
483
484 if (true == string.IsNullOrEmpty(name))
485 {
486 var st = new StackTrace();
487 var sf = st.GetFrame(1);
488
489 name = sf.GetMethod().Name;
490
491 if (name.Equals("MoveNext"))
492 {
493 // We're inside an async method
494 name = sf.GetMethod().ReflectedType.Name
495 .Split(new char[] { '<', '>' }, StringSplitOptions.RemoveEmptyEntries)[0];
496 }
497 }
498
499 // one event, one entry. working out what a caller handed over was for a Log a workflow could
500 // call, and a workflow raises events now - a grid or a class in a file is what the converter is for
501 List<string> entries = new List<string> { logThis ?? "NULL" };
502
503 if (null != Filename)
504 {
505 logFilePath = GetLogFilePath();
506
507 // where this entry went, so a workflow can attach or move the file without working the
508 // folder and the date out for itself. the path is recorded as it is written, not worked out
509 // on request: GetLogFilePath creates folders and reads the clock, so asking before the first
510 // entry would make empty folders and asking after midnight would name a file nobody wrote
511 LogFullPath = logFilePath;
512 LogDirectory = Path.GetDirectoryName(logFilePath);
513 LogFilename = Path.GetFileName(logFilePath);
514
515 if (LogDateFormat.NOTSET != LogDateFormat)
516 formattedDate = string.Format("{0:" + GetFormatString(LogDateFormat) + "}", currentDate);
517 else
518 formattedDate = string.Format("{0:" + GetFormatString(LogDateFormat.MDY_HMS) + "}", currentDate);
519
520 // written as text, which is all a log entry is. the converter used to own this write, and a line
521 // that needs no conversion paid for all of it: the entry was handed back to be detected, so a
522 // "name: value" read as yaml, the extension chose a format a line at a time could not build, and
523 // the write announced itself into the very file it was writing.
524 // converting a finished log to another format is its own step, run by someone who wants it
525 // spaced, so a line reads rather than having to be picked apart by eye
526 string separator = $" {Delimiter ?? "|"} ";
527 StringBuilder lines = new StringBuilder();
528
529 foreach (string entry in entries)
530 lines.Append($"{formattedDate}{separator}{entryType}{separator}{name}{separator}{entry}{Environment.NewLine}");
531
532 File.AppendAllText(logFilePath, lines.ToString());
533 }
534 else if (null != Database)
535 {
536 // one value, with the type in front of it. a database fills in its own timestamp but has no way
537 // to know what kind of event this was, so the entry carries it - and one parameter means one
538 // insert to write, rather than a table having to declare one or two depending
539 IGPALGrid<string> rows = GPAL.Grid.ToGPALObject();
540
541 foreach (string entry in entries)
542 rows.AddRow(new List<string> { $"{entryType} {Delimiter ?? "|"} {entry}" });
543
544 converter.WithInput(rows);
545 converter.SaveTo(Database);
546 }
547 else if (false == noTargetReported)
548 {
549 // nowhere to write it. said once: attached to the publish path it would otherwise raise an error
550 // for every event in the run, and each of those is another event
551 noTargetReported = true;
552
553 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $"Nothing logged: this logger was given no file and no database to write to", this, GPALObjectType.Logger);
554 }
555
556 // nothing published about having logged. a logger saying it logged is a diagnostic whose one reliable
557 // destination is a log file, and with a logger on the publish path it lands in someone else's
558 }
559
560 // Helper method to get week number
561 private int GetWeekNumber(DateTime date)
562 {
563 return (date.DayOfYear - 1) / 7 + 1;
564 }
565 }
566}
567
File-side plumbing behind the fluent chain: writing a unit of work's data out in a delimited format,...
Definition FileHelper.cs:55
static string EnsureDirectoryEndsWithBackslash(string directoryPath)
Adds a trailing backslash to a directory path when it does not already have one, so the path can be c...
Class to define database usage. Currently only used for input from a table, sql or stored procedure....
Everything starts here, all the GPAL controls and global settings using fluent syntax are here....
Definition GPAL.cs:49
static IAllowConverterInput Converter
New GPAL Convertor.
Definition GPAL.cs:571
static void PublishSimpleEvent(GPALEventType gPALEventType, string msg, dynamic gPALObject=null, Enums.GPALObjectType gPALObjectType=GPALObjectType.None, Exception ex=null)
Publish a message to either the information channel or exception channel (if exception passed in) Pub...
Definition GPAL.cs:2568
Provides configurable logging to a file (in any format supported by GPALConverter) or to a GPALDataba...
Definition GPALLogger.cs:47
IAllowLoggingSettings WithLoggingFormat(LogDateFormat logDateFormat)
Sets the date/time format applied to the timestamp prepended to each log entry by Log(dynamic)....
string LogFullPath
LogDirectory and LogFilename joined: the file the last entry went into. Null until something has been...
Definition GPALLogger.cs:71
IAllowLoggingSettings WithFilenamePattern(FilenamePattern pattern)
Sets a timestamp pattern that is appended to the filename (before the extension) each time a log file...
IAllowLoggingSettings WithDelimiter(char delimiter)
Sets the field delimiter used when writing log entries to a delimited text format....
string LogFilename
The name of the file the last entry was written to, with FilenamePattern applied, e....
Definition GPALLogger.cs:66
IAllowLoggingSettings WithFilename(string name)
Sets the base filename (including extension) used for log files. The extension determines the output ...
IAllowLoggingSettings WithRootDirectory(string path)
Sets the root directory under which log files are written. Subdirectories may be appended below this ...
IAllowLoggingSettings WithLogEventTypes(GPALEventType logEventTypes)
Which event types this logger writes. Anything outside the set is dropped, whether GPAL published it ...
IAllowLoggingSettings WithDirectoryStructure(DirectoryStructure type)
Sets the directory structure (e.g. year/month/day) created beneath the root directory for log files....
IAllowLoggingSettings WithDatabase(GPALDatabase database)
Configures the logger to write log entries to a GPALDatabase instead of a file. When set,...
static string GetFormatString(LogDateFormat format)
Translates a LogDateFormat value into the corresponding DateTime format string.
string LogDirectory
The folder the last entry was written to, with DirectoryStructure applied, e.g. logs\231....
Definition GPALLogger.cs:61
IGPALLogger ToGPALObject()
Returns this instance as an IGPALLogger, completing the fluent configuration chain.
Definition GPALLogger.cs:85