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
47 public class GPALLogger : IGPALLogger
48 {
50 private string RootDirectory { get; set; }
51 private DirectoryStructure DirectoryStructure { get; set; }
52 private string Filename { get; set; }
53 private FilenamePattern FilenamePattern { get; set; }
54 private string Delimiter { get; set; }
55 private GPALDatabase Database { get; set; }
56 private LogDateFormat LogDateFormat { get; set; } = LogDateFormat.NOTSET;
57 private bool LogDateAsAttribute { get; set; } = false;
58
63 public string LogDirectory { get; private set; }
68 public string LogFilename { get; private set; }
73 public string LogFullPath { get; private set; }
74
75 private bool noTargetReported = false;
76
77 // one per logger, not one for all of them. writing publishes events of its own, and with a logger
78 // attached to the publish path that lands in another logger's Log - which took this same converter,
79 // reset its input and output, and left the write that was already in progress to save the wrong thing
80 private GPALConverter converter = new GPALConverter();
81 private LogType LogType { get; set; } = LogType.INFO;
82 private GPALEventType LogEventTypes { get; set; } = GPALEventType.All;
83
89 {
90 return this;
91 }
92
96 internal GPALLogger()
97 {
98 IGPALConverter converter = GPAL.Converter.ToGPALObject();
99 // Default values
100 RootDirectory = Directory.GetCurrentDirectory();
101 DirectoryStructure = DirectoryStructure.None;
102 FilenamePattern = FilenamePattern.None;
103 }
118 public IAllowLoggingAndSetting WithLogEventTypes(GPALEventType logEventTypes)
119 {
120 LogEventTypes = logEventTypes;
121 return this;
122 }
123
124 public IAllowLoggingAndSetting WithLogType(LogType logType)
125 {
126 LogType = logType;
127 return this;
128 }
136 {
137 Database = database;
138 return this;
139 }
140
147 {
148 RootDirectory = path;
149 return this;
150 }
151
158 public IAllowLoggingSettings WithDirectoryStructure(DirectoryStructure type)
159 {
160 DirectoryStructure = type;
161 return this;
162 }
163
171 {
172 Filename = name;
173 return this;
174 }
175
182 public IAllowLoggingSettings WithFilenamePattern(FilenamePattern pattern)
183 {
184 FilenamePattern = pattern;
185 return this;
186 }
187
194 {
195 Delimiter = delimiter.ToString();
196 converter.ConverterSettings.InDelimiter = delimiter;
197 return this;
198 }
199
205 public IAllowLoggingAndSetting WithLoggingFormat(LogDateFormat logDateFormat)
206 {
207 LogDateFormat = logDateFormat;
208 return this;
209 }
210
216 public IAllowLoggingAndSetting WithLogDateAsAttribute(bool logDateAsAttribute = true)
217 {
218 LogDateAsAttribute = logDateAsAttribute;
219 return this;
220 }
221
227 private string GetLogFilePath()
228 {
229 string logDirectory = FileHelper.EnsureDirectoryEndsWithBackslash(RootDirectory);
230 string year = DateTime.Now.ToString("yyyy");
231 string month = DateTime.Now.ToString("MM");
232 string day = DateTime.Now.ToString("dd");
233 string hr = DateTime.Now.ToString("HH");
234 string julian = DateTime.Now.DayOfYear.ToString("000");
235
236
237 // Create directory if it doesn't exist
238 Directory.CreateDirectory(logDirectory);
239
240 // Adjust directory structure based on the selected type
241 switch (DirectoryStructure)
242 {
243 case DirectoryStructure.YM:
244 // Create year and month directories
245 logDirectory = Path.Combine(logDirectory, year);
246 Directory.CreateDirectory(logDirectory);
247
248 logDirectory = Path.Combine(logDirectory, month);
249 Directory.CreateDirectory(logDirectory);
250
251 break;
252 case DirectoryStructure.YMD:
253 // Create year, month, and day directories
254 logDirectory = Path.Combine(logDirectory, year);
255 Directory.CreateDirectory(logDirectory);
256
257 logDirectory = Path.Combine(logDirectory, month);
258 Directory.CreateDirectory(logDirectory);
259
260 logDirectory = Path.Combine(logDirectory, day);
261 Directory.CreateDirectory(logDirectory);
262 break;
263 case DirectoryStructure.YMDH:
264 // Create year, month, day, and hour directories
265 logDirectory = Path.Combine(logDirectory, year);
266 Directory.CreateDirectory(logDirectory);
267
268 logDirectory = Path.Combine(logDirectory, month);
269 Directory.CreateDirectory(logDirectory);
270
271 logDirectory = Path.Combine(logDirectory, day);
272 Directory.CreateDirectory(logDirectory);
273
274 logDirectory = Path.Combine(logDirectory, hr);
275 Directory.CreateDirectory(logDirectory);
276 break;
277 case DirectoryStructure.MDH:
278 logDirectory = Path.Combine(logDirectory, month);
279 Directory.CreateDirectory(logDirectory);
280
281 logDirectory = Path.Combine(logDirectory, day);
282 Directory.CreateDirectory(logDirectory);
283
284 logDirectory = Path.Combine(logDirectory, hr);
285 Directory.CreateDirectory(logDirectory);
286 break;
287 case DirectoryStructure.MD:
288 logDirectory = Path.Combine(logDirectory, month);
289 Directory.CreateDirectory(logDirectory);
290
291 logDirectory = Path.Combine(logDirectory, day);
292 Directory.CreateDirectory(logDirectory);
293 break;
294 case DirectoryStructure.M:
295 logDirectory = Path.Combine(logDirectory, month);
296 Directory.CreateDirectory(logDirectory);
297 break;
298 case DirectoryStructure.DH:
299 logDirectory = Path.Combine(logDirectory, day);
300 Directory.CreateDirectory(logDirectory);
301
302 logDirectory = Path.Combine(logDirectory, hr);
303 Directory.CreateDirectory(logDirectory);
304 break;
305 case DirectoryStructure.D:
306 logDirectory = Path.Combine(logDirectory, day);
307 Directory.CreateDirectory(logDirectory);
308 break;
309 case DirectoryStructure.H:
310 logDirectory = Path.Combine(logDirectory, hr);
311 Directory.CreateDirectory(logDirectory);
312 break;
313 case DirectoryStructure.Julian:
314 logDirectory = Path.Combine(logDirectory, julian);
315 Directory.CreateDirectory(logDirectory);
316 break;
317 case DirectoryStructure.None:
318 break;
319 default:
320 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Unable to figure out directory structure [{DirectoryStructure}]", this, GPALObjectType.Logger);
321 break;
322 }
323
324 // Generate filename based on pattern
325 string timestamp = string.Empty;
326 string fileExtension = Filename.Split('.').Last();
327 string filenameWithoutExtension = Filename.Substring(0, Filename.LastIndexOf('.'));
328
329 switch (FilenamePattern)
330 {
331 case FilenamePattern.YMDHMSm:
332 timestamp = DateTime.Now.ToString("yyyyMMddHHmmssfff");
333 break;
334 case FilenamePattern.YMDHMS:
335 timestamp = DateTime.Now.ToString("yyyyMMddHHmmss");
336 break;
337 case FilenamePattern.YMDHM:
338 timestamp = DateTime.Now.ToString("yyyyMMddHHmm");
339 break;
340 case FilenamePattern.YMDH:
341 timestamp = DateTime.Now.ToString("yyyyMMddHH");
342 break;
343 case FilenamePattern.YMD:
344 timestamp = DateTime.Now.ToString("yyyyMMdd");
345 break;
346 case FilenamePattern.YM:
347 timestamp = DateTime.Now.ToString("yyyyMM");
348 break;
349 case FilenamePattern.MM:
350 timestamp = DateTime.Now.ToString("MM");
351 break;
352 case FilenamePattern.Y:
353 timestamp = DateTime.Now.ToString("yyyy");
354 break;
355 case FilenamePattern.MDHMSm:
356 timestamp = DateTime.Now.ToString("MMddHHmmssfff");
357 break;
358 case FilenamePattern.MDHMS:
359 timestamp = DateTime.Now.ToString("MMddHHmmss");
360 break;
361 case FilenamePattern.MDHM:
362 timestamp = DateTime.Now.ToString("MMddHHmm");
363 break;
364 case FilenamePattern.MDH:
365 timestamp = DateTime.Now.ToString("MMddHH");
366 break;
367 case FilenamePattern.DHMSm:
368 timestamp = DateTime.Now.ToString("ddHHmmssfff");
369 break;
370 case FilenamePattern.DHMS:
371 timestamp = DateTime.Now.ToString("ddHHmmss");
372 break;
373 case FilenamePattern.DHM:
374 timestamp = DateTime.Now.ToString("ddHHmm");
375 break;
376 case FilenamePattern.DH:
377 timestamp = DateTime.Now.ToString("ddHH");
378 break;
379 case FilenamePattern.D:
380 timestamp = DateTime.Now.Day.ToString();
381 break;
382 case FilenamePattern.HMSm:
383 timestamp = DateTime.Now.ToString("HHmmssfff");
384 break;
385 case FilenamePattern.HMS:
386 timestamp = DateTime.Now.ToString("HHmmss");
387 break;
388 case FilenamePattern.HM:
389 timestamp = DateTime.Now.ToString("HHmm");
390 break;
391 case FilenamePattern.H:
392 timestamp = DateTime.Now.ToString("HH");
393 break;
394 case FilenamePattern.MSm:
395 timestamp = DateTime.Now.ToString("mmssfff");
396 break;
397 case FilenamePattern.MS:
398 timestamp = DateTime.Now.ToString("mmss");
399 break;
400 case FilenamePattern.M:
401 timestamp = DateTime.Now.ToString("mm");
402 break;
403 case FilenamePattern.Sm:
404 timestamp = DateTime.Now.ToString("ssfff");
405 break;
406 case FilenamePattern.S:
407 timestamp = DateTime.Now.ToString("ss");
408 break;
409 case FilenamePattern.m:
410 timestamp = DateTime.Now.ToString("fff");
411 break;
412 case FilenamePattern.Julian:
413 timestamp = julian;
414 break;
415 case FilenamePattern.None:
416 return Path.Combine(logDirectory, Filename);
417 default:
418 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Unable to determine filename patter [{FilenamePattern}]. Using [{Filename}].", this, GPALObjectType.Logger);
419 return Path.Combine(logDirectory, Filename);
420 }
421
422 if (!string.IsNullOrEmpty(timestamp))
423 filenameWithoutExtension = $"{filenameWithoutExtension}_{timestamp}";
424
425 return Path.Combine(logDirectory, $"{filenameWithoutExtension}.{fileExtension}");
426 }
433 public static string GetFormatString(LogDateFormat format)
434 {
435 switch (format)
436 {
437 case LogDateFormat.YMD_HMS:
438 return "yyyy-MM-dd HH:mm:ss";
439 case LogDateFormat.YMD_HMS_FFF:
440 return "yyyy-MM-dd HH:mm:ss.fff";
441 case LogDateFormat.DMY_HMS:
442 return "dd-MM-yyyy HH:mm:ss";
443 case LogDateFormat.MDY_HMS:
444 return "MM-dd-yyyy HH:mm:ss";
445 case LogDateFormat.DMY:
446 return "dd-MM-yyyy";
447 case LogDateFormat.MDY:
448 return "MM-dd-yyyy";
449 case LogDateFormat.YMD:
450 return "yyyy-MM-dd";
451 case LogDateFormat.HMS:
452 return "HH:mm:ss";
453 case LogDateFormat.HM:
454 return "HH:mm";
455 case LogDateFormat.MDY_Long:
456 return "MMMM dd, yyyy";
457 default:
458 throw new ArgumentException("Unknown LogDateFormat value");
459 }
460 }
461
479 // one writer at a time. every entry arrives on whatever thread published it, and GPAL.Workflow and
480 // the puppeteer receive loop both publish while a workflow is running. AppendAllText opens the file
481 // with FileShare.Read, so a second thread writing at the same moment is refused and that entry is
482 // lost. static rather than per instance, because two loggers pointed at one filename collide too
483 private static readonly object writeLock = new object();
484
487 internal IAllowLoggingSettings Log(string logThis, string raisedBy, LogType entryType)
488 {
489 lock (writeLock)
490 WriteEntry(logThis, raisedBy, entryType);
491
492 return this;
493 }
494
495 private void WriteEntry(string logThis, string raisedBy, LogType entryType)
496 {
497 // the names are kept the same on both enums so this maps without a table. an entry whose type is not
498 // in the set is not written
499 if (false == Enum.TryParse(entryType.ToString(), out GPALEventType logEventType) || false == LogEventTypes.HasFlag(logEventType))
500 return;
501
502 DateTime currentDate = DateTime.Now;
503 string formattedDate;
504 string logFilePath = null;
505
506 // whoever raised the event, which the publisher already worked out. the stack only knows who called
507 // Log, and that is always the publisher now, so every entry would say PublishSimpleEvent
508 string name = raisedBy;
509
510 if (true == string.IsNullOrEmpty(name))
511 {
512 var st = new StackTrace();
513 var sf = st.GetFrame(1);
514
515 name = sf.GetMethod().Name;
516
517 if (name.Equals("MoveNext"))
518 {
519 // We're inside an async method
520 name = sf.GetMethod().ReflectedType.Name
521 .Split(new char[] { '<', '>' }, StringSplitOptions.RemoveEmptyEntries)[0];
522 }
523 }
524
525 // one event, one entry. working out what a caller handed over was for a Log a workflow could
526 // call, and a workflow raises events now - a grid or a class in a file is what the converter is for
527 List<string> entries = new List<string> { logThis ?? "NULL" };
528
529 if (null != Filename)
530 {
531 logFilePath = GetLogFilePath();
532
533 // where this entry went, so a workflow can attach or move the file without working the
534 // folder and the date out for itself. the path is recorded as it is written, not worked out
535 // on request: GetLogFilePath creates folders and reads the clock, so asking before the first
536 // entry would make empty folders and asking after midnight would name a file nobody wrote
537 LogFullPath = logFilePath;
538 LogDirectory = Path.GetDirectoryName(logFilePath);
539 LogFilename = Path.GetFileName(logFilePath);
540
541 if (LogDateFormat.NOTSET != LogDateFormat)
542 formattedDate = string.Format("{0:" + GetFormatString(LogDateFormat) + "}", currentDate);
543 else
544 formattedDate = string.Format("{0:" + GetFormatString(LogDateFormat.MDY_HMS) + "}", currentDate);
545
546 // written as text, which is all a log entry is. the converter used to own this write, and a line
547 // that needs no conversion paid for all of it: the entry was handed back to be detected, so a
548 // "name: value" read as yaml, the extension chose a format a line at a time could not build, and
549 // the write announced itself into the very file it was writing.
550 // converting a finished log to another format is its own step, run by someone who wants it
551 // spaced, so a line reads rather than having to be picked apart by eye
552 string separator = $" {Delimiter ?? "|"} ";
553 StringBuilder lines = new StringBuilder();
554
555 foreach (string entry in entries)
556 lines.Append($"{formattedDate}{separator}{entryType}{separator}{name}{separator}{entry}{Environment.NewLine}");
557
558 File.AppendAllText(logFilePath, lines.ToString());
559 }
560 else if (null != Database)
561 {
562 // one value, with the type in front of it. a database fills in its own timestamp but has no way
563 // to know what kind of event this was, so the entry carries it - and one parameter means one
564 // insert to write, rather than a table having to declare one or two depending
565 IGPALGrid<string> rows = GPAL.Grid.ToGPALObject();
566
567 foreach (string entry in entries)
568 rows.AddRow(new List<string> { $"{entryType} {Delimiter ?? "|"} {entry}" });
569
570 converter.WithInput(rows);
571 converter.SaveTo(Database);
572 }
573 else if (false == noTargetReported)
574 {
575 // nowhere to write it. said once: attached to the publish path it would otherwise raise an error
576 // for every event in the run, and each of those is another event
577 noTargetReported = true;
578
579 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $"Nothing logged: this logger was given no file and no database to write to", this, GPALObjectType.Logger);
580 }
581
582 // nothing published about having logged. a logger saying it logged is a diagnostic whose one reliable
583 // destination is a log file, and with a logger on the publish path it lands in someone else's
584 }
585
586 // Helper method to get week number
587 private int GetWeekNumber(DateTime date)
588 {
589 return (date.DayOfYear - 1) / 7 + 1;
590 }
591 }
592}
593
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:560
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:2406
Provides configurable logging to a file (in any format supported by GPALConverter) or to a GPALDataba...
Definition GPALLogger.cs:48
IAllowLoggingAndSetting WithLogEventTypes(GPALEventType logEventTypes)
Sets the Enums.LogType (e.g. INFO, WARNING, ERROR) to be used for the next call to Log(dynamic)....
string LogFullPath
LogDirectory and LogFilename joined: the file the last entry went into. Null until something has been...
Definition GPALLogger.cs:73
IAllowLoggingSettings WithFilenamePattern(FilenamePattern pattern)
Sets a timestamp pattern that is appended to the filename (before the extension) each time a log file...
IAllowLoggingAndSetting 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:68
IAllowLoggingSettings WithFilename(string name)
Sets the base filename (including extension) used for log files. The extension determines the output ...
IAllowLoggingAndSetting WithLogDateAsAttribute(bool logDateAsAttribute=true)
Controls whether the log timestamp is written as an attribute rather than a regular field when the ou...
IAllowLoggingSettings WithRootDirectory(string path)
Sets the root directory under which log files are written. Subdirectories may be appended below this ...
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,...
IAllowLoggingAndSetting WithLoggingFormat(LogDateFormat logDateFormat)
Sets the date/time format applied to the timestamp prepended to each log entry by Log(dynamic)....
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:63
IGPALLogger ToGPALObject()
Returns this instance as an IGPALLogger, completing the fluent configuration chain.
Definition GPALLogger.cs:88