{"id":18009830,"url":"https://github.com/kekyo/forestlog","last_synced_at":"2025-03-26T14:31:32.005Z","repository":{"id":64250825,"uuid":"573245687","full_name":"kekyo/ForestLog","owner":"kekyo","description":"A minimalist structuring logger interface, binds on Json Lines.","archived":false,"fork":false,"pushed_at":"2023-05-22T02:57:42.000Z","size":377,"stargazers_count":4,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-03-21T23:21:55.603Z","etag":null,"topics":["aspnet-bindings","asynchronous","continuous-streams","dotnet","jsonlines-data","logger","mqttnet-bindings","rotation","suspend-resume"],"latest_commit_sha":null,"homepage":"","language":"C#","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/kekyo.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2022-12-02T02:25:30.000Z","updated_at":"2024-04-24T15:54:15.000Z","dependencies_parsed_at":"2024-10-30T02:32:06.499Z","dependency_job_id":null,"html_url":"https://github.com/kekyo/ForestLog","commit_stats":{"total_commits":91,"total_committers":1,"mean_commits":91.0,"dds":0.0,"last_synced_commit":"fc9e0627188b124ae552f09ad5dc4416a5cd3b32"},"previous_names":[],"tags_count":29,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kekyo%2FForestLog","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kekyo%2FForestLog/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kekyo%2FForestLog/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kekyo%2FForestLog/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/kekyo","download_url":"https://codeload.github.com/kekyo/ForestLog/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":245670726,"owners_count":20653411,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["aspnet-bindings","asynchronous","continuous-streams","dotnet","jsonlines-data","logger","mqttnet-bindings","rotation","suspend-resume"],"created_at":"2024-10-30T02:11:11.180Z","updated_at":"2025-03-26T14:31:31.524Z","avatar_url":"https://github.com/kekyo.png","language":"C#","funding_links":[],"categories":[],"sub_categories":[],"readme":"# ForestLog\n\n![ForestLog](Images/ForestLog.100.png)\n\nForestLog - A minimalist structuring logger interface, binds on Json Lines.\n\n[![Project Status: Active – The project has reached a stable, usable state and is being actively developed.](https://www.repostatus.org/badges/latest/active.svg)](https://www.repostatus.org/#active)\n\n## NuGet\n\nMinimum packages:\n\n| Package  | NuGet                                                                                                                |\n|:---------|:---------------------------------------------------------------------------------------------------------------------|\n| ForestLog | [![NuGet ForestLog](https://img.shields.io/nuget/v/ForestLog.svg?style=flat)](https://www.nuget.org/packages/ForestLog) |\n| ForestLog.JsonLines | [![NuGet ForestLog.JsonLines](https://img.shields.io/nuget/v/ForestLog.JsonLines.svg?style=flat)](https://www.nuget.org/packages/ForestLog.JsonLines) |\n\n3rd party logger binding:\n\n| Package  | NuGet                                                                                                                |\n|:---------|:---------------------------------------------------------------------------------------------------------------------|\n| ForestLog.Extensions.Logging (ASP.NET Core) | [![NuGet ForestLog.Extensions.Logging](https://img.shields.io/nuget/v/ForestLog.Extensions.Logging.svg?style=flat)](https://www.nuget.org/packages/ForestLog.Extensions.Logging) |\n| ForestLog.MQTTnet312 | [![NuGet ForestLog.MQTTnet312](https://img.shields.io/nuget/v/ForestLog.MQTTnet312.svg?style=flat)](https://www.nuget.org/packages/ForestLog.MQTTnet312) |\n\n----\n\n## What is this?\n\nForestLog is a log controller that outputs [Json Lines (`*.jsonl`) format.](https://jsonlines.org/)\nThe format is a de-facto to use continuous data stream, easy-to-use and sufficient.\n\n### What is Json Lines format?\n\nJson Lines are line-separated Json format files that are easy to parse and suitable for recording continuous data.\nSince the data is same as Json, it can handle structured data sets.\nThis means that arbitrary data can be added to the log output, and the log can be mechanically processed later.\n\n```jsonc\n{ \"id\": \"12345\", ... } [LF]\n{ \"id\": \"12346\", ... } [LF]\n{ \"id\": \"12347\", ... } [LF]\n{ \"id\": \"12348\", ... } [LF]\n// ...\n```\n\nForestLog has taken into account that logs containing arbitrary data can be output very easily.\nAnd we took care not to make log file configuration management too complicated to handle (Everything is programmable.)\n\nIt is also suitable for self-hosted applications, mobile applications,\nand the 3rd party binding makes it easy to connect to ASP.NET Core and MQTTnet.\nIt would also be easy to combine with logging systems not included in the binding packages.\n\n### Features\n\nFocus on API comprehensiveness. It has the following features:\n\n* Arbitrary structured data can be added to the log in Json Lines format.\n  * Easily edit structured data with a text editor.\n  * Flexible log data extraction with `sed`, `awk`, `jq`, etc.\n* Flexible API interfaces for various log output situations:\n  * Simple and easy message output methods for commonly expected logger system.\n  * Explicit methods for different log levels and/or programmable log level methods.\n  * Scoped methods that allow the scope of the log to be defined at compile time.\n  * Methods that can log exception objects, including nested exceptions.\n  * Null support in the logger interface; null cases are ignored, so there is no need to implement a separate decision.\n  * Asynchronous versions of all of the above methods.\n* Programmable query interface for logging data sets available.\n* Log size limits and rotations can be specified.\n* Suspend and resume support for log controllers. Fits into the application lifecycle of mobile platforms.\n* Supports third-party bindings (ASP.NET Core, MQTTnet).\n* Fully asynchronous operation is ready.\n  * All log output is processed in the background.\n  * We can await for output expliticly log entries to log file.\n* Only contains 100% managed code. Independent of any external libraries other than the BCL and its compliant libraries.\n* Wide range for target platforms between .NET Framework 3.5 to .NET 7, and .NET Standards.\n\n### Operating Environment\n\nCore interface library:\n\n* .NET 7, 6, 5\n* .NET Core 3.1, 3.0, 2.2, 2.1, 2.0\n* .NET Standard 2.1, 2.0, 1.6, 1.3\n* .NET Framework 4.8, 4.6.1, 4.5, 4.0, 3.5\n\n(Included Xamarin/MAUI platforms)\n\n3rd party bridging interface:\n\n* ASP.NET Core 1.0 or upper\n* MQTTnet 3.1.2\n  * Currently other versions is not supported, because they are contained breaking changes.\n\n### Basic usage\n\nInstall [ForestLog](https://www.nuget.org/packages/ForestLog) and [ForestLog.JsonLines](https://www.nuget.org/packages/ForestLog.JsonLines) packages.\n\nWe need to create \"Log controller\" from the factory:\n\n```csharp\nusing ForestLog;\n\n// Construct log controller:\nusing ILogController logController = LogController.Factory.CreateJsonLines(\n    // Output base directory path.\n    \"logs\",\n    // Minimum output log level.\n    LogLevels.Debug);\n```\n\nThen, create a logger interface and ready to output:\n\n```csharp\n// Create logger:\nILogger logger = logController.CreateLogger();\n\n// Write log entries:\nvar arg1 = 123;\nvar arg2 = 456;\nlogger.Debug($\"Always using string interpolation: {arg1}\");\nlogger.Trace($\"Always using string interpolation: {arg2}\");\n```\n\nResult in base directory `log.jsonl` (Json Lines format):\n\n```jsonc\n{\n    \"id\": \"0a913e2e-4ba7-4606-b703-2c9eccc9d217\",\n    \"facility\": \"default\",\n    \"logLevel\": \"debug\",\n    \"timestamp\": \"2022-12-06T09:27:04.5451256+09:00\",\n    \"scopeId\": 1,\n    \"message\": \"Always using string interpolation: 123\",\n    \"memberName\": \"PurchaseProductAsync\",\n    \"filePath\": \"D:\\\\Projects\\\\AwsomeItemSite\\\\AwsomeItemSite.cs\",\n    \"line\": 229,\n    \"managedThreadId\": 16,\n    \"nativeThreadId\": 11048,\n    \"taskId\": -1,\n    \"processId\": 43608\n}\n{\n    \"id\": \"31b4709f-f7f5-45b5-9381-75f64e23efce\",\n    \"facility\": \"default\",\n    \"logLevel\": \"trace\",\n    \"timestamp\": \"2022-12-06T09:27:04.5473678+09:00\",\n    \"scopeId\": 1,\n    \"message\": \"Always using string interpolation: 456\",\n    \"memberName\": \"PurchaseProductAsync\",\n    \"filePath\": \"D:\\\\Projects\\\\AwsomeItemSite\\\\AwsomeItemSite.cs\",\n    \"line\": 230,\n    \"managedThreadId\": 16,\n    \"nativeThreadId\": 11048,\n    \"taskId\": -1,\n    \"processId\": 43608\n}\n```\n\n----\n\n## Attach additional structured data\n\nYou can output with any additional instances:\n\n```csharp\n// Write log entry with additional data:\nlogger.Information($\"See additional data below\",\n    new {\n        Amount = 123,\n        Message = \"ABC\",\n        NameOfProduct = \"PAC-MAN quarter\",\n    });\n```\n\nResult:\n\n```jsonc\n{\n    \"message\": \"See additional data below\",\n    \"additionalData\": {\n        \"amount\": 123,\n        \"message\": \"ABC\",\n        \"nameOfProduct\": \"PAC-MAN quarter\"\n    },\n    // ...\n}\n```\n\nThe instance will be serialized by [NewtonSoft.Json](https://json.net/),\nso you can use your existing knowledge to customize the Json representation.\n\n----\n\n## Attach any exceptions\n\nThe interfaces have feature for exception attachable:\n\n```csharp\ntry\n{\n    throw new ApplicationException(\"Failed a operation.\");\n}\ncatch (Exception ex)\n{\n    // (There are also overloads that specify separate messages.)\n    logger.Error(ex);\n}\n```\n\nResult:\n\n```jsonc\n{\n    \"logLevel\": \"error\",\n    \"message\": \"System.ApplicationException: Failed a operation.\",\n    \"additionalData\": {\n        \"name\": \"System.ApplicationException\",\n        \"message\": \"Failed a operation.\",\n        \"stackFrames\": [\n            \"at AwsomeItemSite.Transaction.TransactAsync() at D:\\\\Projects\\\\AwsomeItemSite\\\\Transaction.cs:line 55\"\n        ],\n        \"innerExceptions\": []\n    },\n    // ...\n}\n```\n\n----\n\n## Indicate explicit log level\n\nThe log level values are:\n\n```csharp\n// The lower symbol name is the most important.\n// This order affects `MinimumOutputLogLevel` limitation.\npublic enum LogLevels\n{\n    Debug,       // |\n    Trace,       // |\n    Information, // |\n    Warning,     // |\n    Error,       // |\n    Fatal,       // v Most important\n    Ignore,      // \u003c-- Will ignore any log output.\n}\n```\n\nThese values can be used to vary the log level:\n\n```csharp\n// Write log with log level variables:\nvar level1 = LogLevels.Debug;\nlogger.Log(level1, $\"Debugging enabled.\");\n\nvar level2 = LogLevels.Warning;\nlogger.Log(level2, $\"Failed the transaction.\");\n```\n\n----\n\n## Annotates facility name\n\n```csharp\n// Create facility annoteted logger\nvar logger = logController.CreateLogger(\"DispatchController\");\n\nvar unitCount = 5;\nlogger.Information($\"Through the valid unit: Units={unitCount}\");\n```\n\nResult:\n\n```jsonc\n{\n    \"facility\": \"DispatchController\",\n    \"logLevel\": \"information\",\n    \"message\": \"Through the valid unit: Units=5\",\n    // ...\n}\n```\n\n----\n\n## Awaited for exactly output\n\nNormally, ForestLog outputs all log entries in the background context.\nThe use of an Awaitable method ensures that the log entries are actually output to a file.\n\n```csharp\npublic async Task OutputAsync(ILogger logger)\n{\n    // We need to wait exactly output critical logs:\n    await logger.InformationAsync($\"Awaited to exactly output.\");\n}\n```\n\n----\n\n## Delayed evaluation\n\nWe can use the `Func\u003cT\u003e` to delay the evaluation of the values to be included in the log:\n\n```csharp\n// If it needs a large cost to calculate:\nlogger.Information(\n    $\"Calculated total density: {() =\u003e this.CalculateDensity(123, 456)}\");\n```\n\n* This expression is actually called when the log is output on worker thread context.\n  It isn't evaluate when the log level is less than the output.\n* This feature availables only C# 10 or upper compiler.\n\n----\n\n## Scoped output\n\nThe scoped output features will apply log entry relations with `scopeId` identity on log key.\nAnd the time between entering and exiting the scope is then measured.\n\n```csharp\npublic void Scope(ILogger parentLogger)\n{\n    parentLogger.TraceScope(logger =\u003e\n    {\n        logger.Debug($\"Output in child scope.\");\n        logger.Warning($\"Same child scope.\");\n    });\n}\n\npublic Task ScopeAsync(ILogger parentLogger)\n{\n    return parentLogger.TraceScopeAsync(async logger =\u003e\n    {\n        logger.Debug($\"Output in child scope.\");\n        logger.Warning($\"Same child scope.\");\n    });\n}\n```\n\nResult:\n\n```jsonc\n{\n    \"logLevel\": \"trace\",\n    \"scopeId\": 123,          // \u003c-- Same scope id\n    \"parentScopeId\": 42,     // \u003c-- Parent logger scope id\n    \"message\": \"Enter.\",\n    // ...\n}\n{\n    \"logLevel\": \"debug\",\n    \"scopeId\": 123,          // \u003c-- Same scope id\n    \"parentScopeId\": 42,\n    \"message\": \"Output in child scope.\",\n    // ...\n}\n{\n    \"logLevel\": \"warning\",\n    \"scopeId\": 123,          // \u003c-- Same scope id\n    \"parentScopeId\": 42,\n    \"message\": \"Same child scope.\",\n    // ...\n}\n{\n    \"logLevel\": \"trace\",\n    \"scopeId\": 123,          // \u003c-- Same scope id\n    \"parentScopeId\": 42,\n    \"message\": \"Leave: Elapsed=00:00:00.00146248\",\n    // ...\n}\n```\n\nThe timestamp from `Enter` to `Leave` in the same `scopeId` can be used to calculate the time at tally time,\nbut elapsed time indicated in `Leave` message is even more precise.\n\nScope output can include arguments, return values and exception information:\n\n```csharp\npublic string Scope(ILogger parentLogger, int a, double b, string c)\n{\n    // Using `new` operator with implicitly type `BlockScopeArguments`.\n    return parentLogger.TraceScope(new(a, b, c), logger =\u003e\n    {\n        return (a + b) + c;\n    });\n}\n```\n\nResult:\n\n```jsonc\n{\n    \"logLevel\": \"trace\",\n    \"scopeId\": 456, \n    \"parentScopeId\": 42, \n    \"message\": \"Enter.\",\n    \"additionalData\": [\n        111,\n        222.333,\n        \"ABC\"\n    ],\n    // ...\n}\n{\n    \"logLevel\": \"trace\",\n    \"scopeId\": 456, \n    \"parentScopeId\": 42, \n    \"message\": \"Leave: Elapsed=00:00:00.00146248\",\n    \"additionalData\": \"333.333ABC\",\n    // ...\n}\n```\n\nWhen leave with exception:\n\n```jsonc\n{\n    \"logLevel\": \"trace\",\n    \"scopeId\": 456, \n    \"parentScopeId\": 42, \n    \"message\": \"Leave with exception: Elapsed=00:00:00.00146248\",\n    \"additionalData\": {\n        \"name\": \"System.ApplicationException\",\n        \"message\": \"Application might has invalid state...\"\n        \"stackFrames\": [\n            \"at AwsomeItemSite.Transaction.TransactAsync() at D:\\\\Projects\\\\AwsomeItemSite\\\\Transaction.cs:line 55\"\n        ],\n        \"innerExceptions\": []\n    },\n    // ...\n}\n```\n\nAlternatively, you can use `IDisposable` to define RAII-like scopes:\n\n```csharp\npublic void Scope(ILogger parentLogger)\n{\n    using (var logger = parentLogger.TraceScope())\n    {\n        logger.Debug($\"Output in child scope.\");\n        logger.Warning($\"Same child scope.\");\n    }\n}\n\npublic async Task ScopeAsync(ILogger parentLogger)\n{\n    using (var logger = await parentLogger.TraceScopeAsync())\n    {\n        logger.Debug($\"Output in child scope.\");\n        logger.Warning($\"Same child scope.\");\n    }\n}\n```\n\nIf you are familiar with the C# language, you may find this method easier to write.\nHowever, that the logger does not record the contents of both the return value and exception details when it occurs.\n(the \"Leave\" only message is recorded when the exception occurs and the scope is exited).\n\n----\n\n## Configure maximum log size and rotation\n\nWill switch log file when current log file size is exceed.\n\n```csharp\nusing var logController = LogController.Factory.CreateJsonLines(\n    \"logs\",\n    LogLevels.Debug,\n    // Size to next file.\n    1 * 1024 * 1024  // bytes\n    );\n```\n\nResult:\n\n![Applied log size configuration](Images/logs_directory.png)\n\nThe current log file to be appended is always `log.jsonl`.\nWhen the file size is exceeded, it is renamed to a numbered file and a new `log.json` file is generated.\n\nEnable log file rotation:\n\n```csharp\nusing var logController = LogController.Factory.CreateJsonLines(\n    \"logs\",\n    LogLevels.Debug,\n    1 * 1024 * 1024,\n    // Maximum log files.\n    10\n    );\n```\n\n----\n\n## Suspend and resume\n\nIn an environment such as smartphones and/or tablet devices,\nlog output must be suspended and resumed as the application transitions between states.\n\nThe following example will correspond to an application transition in Xamarin Android:\n\n```csharp\npublic sealed class MainActivity\n{\n    private readonly ILogController logController =\n        LogController.Factory.CreateJsonLines(...);\n\n    public MainActivity()\n    {\n        DependencyService.RegisterSingleton\u003cILogController\u003e(this.logController);\n    }\n\n    // ...\n\n    protected override void OnPause()\n    {\n        // Suspend log controller.\n        this.logController.Suspend();\n\n        base.OnPause();\n    }\n\n    protected override void OnResume()\n    {\n        base.OnResume();\n\n        // Resume log controller.\n        this.logController.Resume();\n    }\n}\n```\n\n`Suspend()` method writes all queued log entries into the log files (will block while completed).\nAfter that, any logging request will be ignored when before `Resume()` is called.\n\n----\n\n## Programmatically retreive log entries\n\nEvent to monitor log outputted in real time:\n\n```csharp\nlogController.Arrived += (s, e) =\u003e\n{\n    // This thread context is worker thread.\n    // So you have to dispatch UI thread when using GUI frameworks.\n    Console.WriteLine(e.LogEntry.ToString());\n};\n```\n\nOr, perform quering and filtering by predicates from all logs recorded (including outputted to files):\n\n```csharp\nLogEntry[] importantLogs = await logController.QueryLogEntriesAsync(\n    // Maximum number of log entries.\n    100,\n    // Filter function.\n    logEntry =\u003e logEntry.LogLevel \u003e= LogLevels.Warning);\n```\n\n----\n\n## 3rd party logger binding\n\n### ASP.NET Core binding configuration\n\nInstall [ForestLog.Extensions.Logging](https://www.nuget.org/packages/ForestLog.Extensions.Logging) package,\nand configure using with `AddForestLog()` method extension:\n\n```csharp\nusing ForestLog;\n\nusing var logController = LogController.Factory.CreateJsonLines(\n    /* ... */);\n\nvar builder = WebApplication.CreateBuilder();\n\nbuilder.WebHost.\n    ConfigureLogging(builder =\u003e builder.AddForestLog(logController)).\n    UseUrls(\"http://localhost/\");\n\nvar webApplication = builder.Build();\n\n// ...\n```\n\n* Or, you can use `builder.Services.AddForestLog()` directly.\n* Yes, it is implemented for `Microsoft.Extensions.Logging` interfaces.\n  So you can apply this package to ASP.NET Core, Entity Framework Core and any other projects.\n\n### MQTTnet 3.1.2 binding configuration\n\nInstall [ForestLog.MQTTnet312](https://www.nuget.org/packages/ForestLog.MQTTnet312) package,\nand uses `ForestLog.MqttNetLogger` class:\n\n```csharp\nusing ForestLog;\n\nusing var logController = LogController.Factory.CreateJsonLines(\n    /* ... */);\n\nvar mqttClient = new MqttFactory().\n    CreateMqttClient(new MqttNetLogger(logController));\n```\n\n----\n\n## Uses builtin awaitable value task (Advanced topic)\n\nForestLog has its own awaitable type, the `LoggerAwaitable` type.\nThis structure is a value-type likes the `ValueTask` type and allowing low-cost asynchronous operations.\n(Yes, we can use these types on both `net35`, `net40` and `net45` tfms :)\n\nIt also defines an inter-conversion operator between the `Task` and `ValueTask` type,\nallowing seamless use as follows:\n\n```csharp\n// Implicitly conversion from `Task`\nasync Task AsyncOperation()\n{\n    // ...\n}\n\nLoggerAwaitable awaitable = AsyncOperation();\nawait awaitable;\n```\n\n```csharp\n// Implicitly conversion from `ValueTask`\nasync ValueTask AsyncOperation()\n{\n    // ...\n}\n\nLoggerAwaitable awaitable = AsyncOperation();\nawait awaitable;\n```\n\n```csharp\n// Implicitly conversion from `Task\u003cT\u003e`\nasync Task\u003cint\u003e AsyncOperationWithResult()\n{\n    // ...\n}\n\nLoggerAwaitable\u003cint\u003e awaitable = AsyncOperationWithResult();\nvar result = await awaitable;\n```\n\n```csharp\n// async-await operation\nasync LoggerAwaitable AsyncOperation()\n{\n    await Task.Delay(100);\n}\n\nawait AsyncOperation();\n```\n\n```csharp\n// async-await operation with result\nasync LoggerAwaitable\u003cint\u003e AsyncOperationWithResult()\n{\n    await Task.Delay(100);\n    return 123;\n}\n\nvar result = await AsyncOperationWithResult();\n```\n\nThese `LoggerAwaitable` types are defined for the following reasons:\n\n* Elimination of dependencies on assemblies containing `ValueTask` types.\n* Elimination of complications due to inter-conversion between `Task` and `ValueTask` types.\n\nFor example, using the `LoggerAwaitable` type,\nyou can easily (simply) write and reduce asynchronous operation cost the following:\n\n```csharp\n// `TraceScopeAsync` method receives `Func\u003cILogger, LoggerAwaitable\u003cint\u003e\u003e` delegate type\n// and returns `LoggerAwaitable\u003cint\u003e` type.\npublic Task\u003cint\u003e ComplextOperationAsync() =\u003e\n    this.logger.TraceScopeAsync(async logger =\u003e\n    {\n        // ...\n\n        return result;\n    });\n```\n\nNote: In `netcoreapp2.1` or later and `netstandard2.1`,\nthe `ValueTask` is not required any external dependencies.\nSo we can use `ValueTask` conversion naturally on these environments.\n\n----\n\n## Addendum\n\n* Log file output is executed by worker thread,\n  so synchronous versions of log output methods (such as `logger.Trace()`) always will not block.\n  And the serialization process to Json is also performed within the worker thread.\n  Care is taken to affect the thread that requested the log output as little as possible.\n* In `net45` and higher environments, Json generation and output to file are performed in an asynchronous overlapping manner.\n  Even if complex `Additional Data` structures are specified, the output to the file is less affected.\n* Of course, there is no problem with reading logs (`logController.QueryLogEntriesAsync()`) during log output.\n* ForestLog does not have any static log output methods.\n  For example, methods like `StaticLogger.StaticTrace(...)`.\n  We have seen many times that such static log output methods have disastrous results in application development iterations and integration projects.\n  We recommend that instances of the `ILogger` interface be brought around in each implementation.\n\n----\n\n## License\n\nApache-v2.\n\n\n## History\n\n* 1.2.0:\n  * Supported null logger interface and will ignore log writing when not avoids null checking.\n* 1.1.0:\n  * Added more scoping logger methods.\n  * Fixed blocking at forced shutdown with TAE from another thread.\n  * Added DisposeAsync on net45 or greater.\n* 1.0.0:\n  * Initial general release.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkekyo%2Fforestlog","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fkekyo%2Fforestlog","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkekyo%2Fforestlog/lists"}