Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.vs/
118 changes: 83 additions & 35 deletions README.md
Original file line number Diff line number Diff line change
@@ -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.
146 changes: 146 additions & 0 deletions cSyslogClient.cs
Original file line number Diff line number Diff line change
@@ -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<Exception>)
* 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
}