From e869ce531fde494150fcf29da9d949f397b70556 Mon Sep 17 00:00:00 2001 From: Ilya Shulman Date: Thu, 26 Jun 2025 11:35:43 +0300 Subject: [PATCH] Initial commit: Enhanced syslog client with remote logging support --- .gitignore | 1 + README.md | 118 ++++++++++++++++++++++++++------------ cSyslogClient.cs | 146 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 230 insertions(+), 35 deletions(-) create mode 100644 .gitignore create mode 100644 cSyslogClient.cs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a4d6d9c --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.vs/ \ No newline at end of file diff --git a/README.md b/README.md index 8310366..481c83c 100644 --- a/README.md +++ b/README.md @@ -1,67 +1,115 @@ -# SyslogCore +# SyslogCore (Enhanced) -Simple way to write to `syslog` aka `/dev/log` aka `/var/log/syslog` in .NET Core on Linux. Consists of [one short C# file](Syslog.cs) (70 lines!) that you can throw into your project. Tested in ASP.NET Core 5. +Modern syslog client for .NET with dual logging (local libc + remote UDP) and RFC 5424 support. +Fork of [jitbit/SyslogCore](https://github.com/jitbit/SyslogCore) with critical improvements. ## Usage ```csharp -Syslog.Write(Syslog.Level.Warning, "MyAwesomeApp", "something went wrong"); -``` - -## The problem +// 1. One-time initialization +cSyslogClient.Init(appName: "MyApp",remoteServer: "10.0.0.1" // Optional); -.NET Core (aka .NET 5 and later) does not have a [built-in](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/logging/?view=aspnetcore-5.0&tabs=aspnetcore2x#built-in-logging-providers-1) logging provider for linux. The official recommendation is to use a 3rd party logger, like Serilog, NLog, Log4Net etc. +// 2. Logging (auto module detection) +cSyslogClient.Write("Payment processed", SyslogLevel.Info); -All heavyweight large libraries. +// 3. Graceful shutdown +cSyslogClient.Shutdown(); +``` -There's an [ongoing discussion](https://github.com/aspnet/Logging/issues/441) if a default logger should be a part of .NET runtime, but it's stall. +## Key Features -## File logging is tricky +* Dual logging: Local (libc) + Remote (UDP RFC 5424) -Where do you place the logs? How do you grant permissions to that location? How should the files be formatted? When do the files rotate/roll over? Microsoft couldn't decide and... simply ignored the problem. +* Structured messages: Timestamps, hostnames, priority levels -## Enter `syslog` +* Auto caller detection: [CallerMemberName] integration -**Why reinvent the wheel?!** +* Resource-safe: Proper GCHandle and UdpClient disposal -Almost every linux distro comes with a built-in feature called `syslog`. It takes care of everything: queues messages, writes to log files, rotates files (via "logrotate"), and exists on literally every Linux machine. It has lots of ways to send messages to it: UDP-listener, TCP-listener, a "Unix socket" at `/dev/log`, a `logger` CLI command or a `syslog()` system function etc. +* Zero dependencies: Single C# file (~150 LOC) -For Windows folks: think of it as an `EventLog.Write`, *but for Linux* +## Why This Fork? -## Works with docker too! +The original library was limited to local syslog. We added: -If you're running an app inside a container, `docker logs` will show these logs, zero configuration. +🚀 Remote syslog server support -## How do we use it in C#? +📡 RFC 5424 compliance -Just use the good old `DllImport` to reference the external `libc` library, and call the original [syslog](https://linux.die.net/man/3/syslog) function. That's it. No Nugets, no dependency-injection. +🛡️ Safer resource management -Happy coding. +🔍 Automatic caller module tracking -# FAQ +## Usage example -### 1. What if (for some weird reason) syslog is not installed on my Linux? +ASP.NET Core Integration -Run `sudo apt-get install rsyslog` +```csharp +var builder = WebApplication.CreateBuilder(args); +var app = builder.Build(); -### 2. Syslog is present on my machine but there's no logs +cSyslogClient.Init("WebApp", "192.168.113.100"); -`rsyslog` might be present but it's _not running_ (known issue with WSL2 for example). Check that its running, if not - start it: +app.Lifetime.ApplicationStopping.Register(() => +{ + cSyslogClient.Write("Shutting down..."); + cSyslogClient.Shutdown(); +}); -``` -$ service rsyslog status - * rsyslogd is not running -$ sudo service rsyslog start +app.MapGet("/", () => { + cSyslogClient.Write("Homepage visited"); // Auto-detects "[GET] /" as module + return "Hello World"; +}); ``` -Then test that logging actually works: +## Configuration Options +```csharp +cSyslogClient.Init( + appName: "Backend", + remoteServer: "10.0.0.1", // UDP target + remotePort: 8514, // Custom port + customHostName: "server-1" // Override hostname +); +``` +## Expected output ``` -$ logger testtesttest -$ tail -1 /var/log/syslog -Oct 11 13:51:18 DESKTOP-CDBR5NK jazz: testtesttest +2025-06-25T13:31:41.190245+03:00 dev69-u lk[206841]: [INFO] main: startup v0.2.1.0 +2025-06-25T13:50:40.203339+03:00 dev69-u be[207439]: [INFO] main: startup v2.0.0.0 +2025-06-25T13:57:50.985781+03:00 dev69-u be[207439]: [INFO] main(): shutdown +2025-06-25T13:57:51.513765+03:00 dev69-u be[207668]: [INFO] main: startup v2.0.0.0 +2025-06-25T13:58:16.690086+03:00 dev69-u lk[206841]: [INFO] lk-files: Test from filesController +2025-06-25T13:58:23.113230+03:00 dev69-u be[207668]: [INFO] GetFileName: File report.pdf uploaded to caller ``` +## Logging to a Separate File (local0-local7) + +To direct logs to a dedicated file instead of `/var/log/syslog`, use Linux's local facilities (`local0` through `local7`). Example for `local0`: +Create rsyslog config (/etc/rsyslog.d/20-myapp.conf): +```bash +# Logs with facility=local0 to /var/log/myapp.log +local0.* /var/log/myapp.log +& stop # Prevent duplicate logging +``` + +This code uses `Local0` by default. You can change it in: +```csharp +// In LibC.openlog() call: +LibC.openlog(..., facility: 16 << 3); // 16=local0, 17=local1, etc. +``` + +## Performance Notes + +For high-throughput scenarios: + +* Consider batching UDP messages + +* Future upgrade path to LibraryImport (AOT support) + +* Async logging possible via _udpClient.SendAsync + +## Info -### 3. What if I copy paste this into a cross-platform app that runs on both Windows and Linux? +* Original credits: [Jitbit](https://github.com/jitbit/SyslogCore) +* Enhancements by: [ish-1313](https://github.com/ish-1313/) +* License: MIT (same as original) -No worries, the code checks if it runs on Windows or Linux before proceeding. diff --git a/cSyslogClient.cs b/cSyslogClient.cs new file mode 100644 index 0000000..7c9dbe7 --- /dev/null +++ b/cSyslogClient.cs @@ -0,0 +1,146 @@ +/* + * Original work (c) Jitbit - https://github.com/jitbit/SyslogCore + * + * Enhanced by ish-1313 (@ish-1313) - https://github.com/ish-1313/SyslogCore + * Major improvements: + * - Dual logging (local syslog + remote UDP) + * - RFC 5424 compliance (structured messages) + * - CallerMemberName auto-detection + * - Graceful resource cleanup (GCHandle) + * + * License: MIT + */ + +using System.Net; +using System.Net.Sockets; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Text; + +namespace common.Logging; + +/* + * Future improvements (w/o breaking changes): + * 1. LibraryImport для AOT (.NET 7+) + * 2. Error-handling callback (like Action) + * 3. Async-version SendRemoteSyslog + * 4. Batching for UDP + */ +internal static class LibC +{ + + [DllImport("libc")] +#pragma warning disable SYSLIB1054 // Use 'LibraryImportAttribute' instead of 'DllImportAttribute' to generate P/Invoke marshalling code at compile time + public static extern void openlog(IntPtr ident, int option, int facility); + + [DllImport("libc", CharSet = CharSet.Auto)] + public static extern void syslog(int priority, string format, string level, string appname, string message); + + [DllImport("libc", CharSet = CharSet.Auto)] + public extern static void closelog(); +#pragma warning restore SYSLIB1054 +} + +public static class cSyslogClient +{ + const int LOG_PID = 0x01; + const int LOG_CONS = 0x02; + const int LOG_USER = 1 << 3; + const int LOG_LOCAL0 = 16 << 3; + + static IPEndPoint _remoteEndPoint; + static UdpClient _udpClient; + static string _appName; + static string _hostname; + static bool _isLocalEnabled; + static bool _isRemoteEnabled; + + public enum LogTarget + { + LocalOnly, + RemoteOnly, + AllTargets + } + + static GCHandle _handle = default; + static bool _isInited = false; + + public static void Init(string appName, string remoteServer = null, int remotePort = 514, string customHostName = null) + { + _appName = appName; + _hostname = customHostName ?? Dns.GetHostName(); + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + _handle = GCHandle.Alloc(GetNullTerminatedAscii(appName), GCHandleType.Pinned); + LibC.openlog(_handle.AddrOfPinnedObject(), LOG_PID | LOG_CONS, LOG_LOCAL0); + _isLocalEnabled = true; + } + + if (!string.IsNullOrEmpty(remoteServer)) + { + _remoteEndPoint = new IPEndPoint(IPAddress.Parse(remoteServer), remotePort); + _udpClient = new(); + _isRemoteEnabled = true; + } + _isInited = true; + } + public static void Write(string message, SyslogLevel level = SyslogLevel.Info, LogTarget target = LogTarget.AllTargets, [CallerMemberName] string moduleName = "") + { + moduleName = (string.IsNullOrEmpty(moduleName)) ? "Unknown method" : moduleName; + try + { + bool writeLocal = _isLocalEnabled && (target == LogTarget.LocalOnly || target == LogTarget.AllTargets); + bool writeRemote = _isRemoteEnabled && (target == LogTarget.RemoteOnly || target == LogTarget.AllTargets); + + if (writeLocal) LibC.syslog((int)level | LOG_LOCAL0, "[%s] %s: %s", level.ToString().ToUpper(), moduleName, message); + if (writeRemote) SendRemoteSyslog(moduleName, message, level); + } + catch { /* Suppress logging errors */ } + } + + private static void SendRemoteSyslog(string moduleName, string message, SyslogLevel level) + { + try + { + var priority = (int)level + LOG_LOCAL0; + var timestamp = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss.fffZ"); + + var syslogMsg = $"<{priority}>1 {timestamp} {_hostname} {_appName} {moduleName} - - {message}"; + var bytes = Encoding.ASCII.GetBytes(syslogMsg); + + _udpClient?.Send(bytes, bytes.Length, _remoteEndPoint); + } + catch {/* Suppress network errors */ } + } + private static byte[] GetNullTerminatedAscii(string input) + { + byte[] bytes = new byte[input.Length + 1]; // +1 для нуль-терминатора + Encoding.ASCII.GetBytes(input, 0, input.Length, bytes, 0); + return bytes; + } + + public static void Shutdown() + { + if (_isInited) + { + if (_handle != default && _handle.IsAllocated) _handle.Free(); + _udpClient?.Dispose(); + _udpClient = null; + LibC.closelog(); + _isInited = false; + } + } +} + +public enum SyslogLevel +{ + Emergency = 0, + Alert = 1, + Critical = 2, + Error = 3, + Warning = 4, + Notice = 5, + Info = 6, + Debug = 7 +} \ No newline at end of file