yo your .NET logs are giving corporate dystopia energy rn and thats not it bestie. litty-logs fully rewrites all them boring built-in framework messages into gen alpha slang while also blessing your terminal with emojis and ANSI colors no cap
info: Microsoft.Hosting.Lifetime[14]
Now listening on: http://localhost:5000
info: Microsoft.Hosting.Lifetime[0]
Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
Content root path: /app
[๐ฅ info] [2026-02-18T21:45:00.420Z] [Lifetime] we vibing on http://localhost:5000 fr fr ๐ง
[๐ฅ info] [2026-02-18T21:45:00.421Z] [Lifetime] app is bussin and ready to slay bestie ๐
yeet Ctrl+C to dip out no cap
[๐ฅ info] [2026-02-18T21:45:00.421Z] [Lifetime] content root living at /app bestie ๐
dotnet add package LittyLogs
# for xUnit v3 test output (optional, separate package)
dotnet add package LittyLogs.Xunit
# for file sink with rotation and gzip compression (optional, separate package)
dotnet add package LittyLogs.File
# for Slack + Matrix webhook sinks (optional, separate package) ๐ฅ
dotnet add package LittyLogs.Webhooks
# for the CLI tool that litty-fies build, test, publish, pack, and clean output
dotnet tool install --global LittyLogs.Toolusing LittyLogs;
var builder = WebApplication.CreateBuilder(args);
builder.Logging.AddLittyLogs(); // thats it bestie ๐ฅ
var app = builder.Build();
app.Run();using LittyLogs;
var host = Host.CreateDefaultBuilder(args)
.ConfigureLogging(logging => logging.AddLittyLogs())
.ConfigureServices(services => services.AddHostedService<MyService>())
.Build();
await host.RunAsync();using LittyLogs;
using Microsoft.Extensions.Logging;
using var factory = LoggerFactory.Create(logging =>
{
logging.SetMinimumLevel(LogLevel.Trace);
logging.AddLittyLogs();
});
var logger = factory.CreateLogger("MyScript");
logger.LogInformation("we in here bestie ๐ฅ");using LittyLogs.Xunit;
using Xunit;
public class MyTests
{
private readonly ILogger<MyTests> _logger;
public MyTests(ITestOutputHelper output)
{
// one line to litty-fy your test output bestie ๐
_logger = output.CreateLittyLogger<MyTests>();
}
[Fact]
public void MyTest()
{
_logger.LogInformation("this shows up litty-fied in test output ๐ฅ");
}
}same litty rewrites and emojis, but as valid JSON. your log aggregator is gonna love this no cap
using LittyLogs;
var builder = WebApplication.CreateBuilder(args);
builder.Logging.AddLittyJsonLogs(); // structured JSON with emojis bestie ๐ฅ
var app = builder.Build();
app.Run();output:
{"timestamp":"2026-02-19T10:45:00.420Z","level":"info","emoji":"๐ฅ","category":"Lifetime","message":"app is bussin and ready to slay bestie ๐
yeet Ctrl+C to dip out no cap"}emojis in JSON? absolutely bussin โ JSON is UTF-8 native so every parser on earth handles it perfectly ๐
using LittyLogs.File;
var builder = WebApplication.CreateBuilder(args);
builder.Logging.AddLittyFileLogs(opts =>
{
opts.FilePath = "logs/app.log";
opts.OutputFormat = LittyFileOutputFormat.Text; // or Json for structured output
opts.RollingInterval = LittyRollingInterval.Daily;
opts.MaxFileSizeBytes = 10 * 1024 * 1024; // 10MB then rotate
opts.CompressionMode = LittyCompressionMode.Gzip; // compress rotated files ๐๏ธ
});
var app = builder.Build();
app.Run();features that go hard:
-
async I/O โ
Channel<string>based, your app thread never blocks on disk writes ๐ - text or JSON โ human-readable or machine-parseable, your choice bestie
- size + time rotation โ daily, hourly, or size-based. rotated files get timestamps in the name
-
gzip compression โ old rotated files auto-compress to
.gz, active file stays uncompressed - startup safeguard โ never auto-rotates on startup, only rotates before writing the next entry ๐
- no ANSI codes โ files never get terminal escape chars, thats cursed ๐
critical error hits? your group chat knows about it instantly, formatted all nice with emojis
using LittyLogs.Webhooks;
var builder = WebApplication.CreateBuilder(args);
builder.Logging.AddLittyMatrixLogs("https://hookshot.example.com/webhook/abc123"); // Matrix one-liner ๐ฃ๐ฅ
builder.Logging.AddLittySlackLogs("https://hooks.slack.com/services/your/secret/path"); // Slack one-liner ๐ข๐ฅ
var app = builder.Build();
app.Run();with full options:
builder.Logging.AddLittySlackLogs("https://hooks.slack.com/services/your/secret/path", opts =>
{
opts.MinimumLevel = LogLevel.Warning; // only Warning+ goes to chat (default)
opts.Username = "Deploy Alerts"; // Slack message header, not the app identity
opts.BatchSize = 10; // 49 max so header + logs stay under 50 blocks
opts.BatchInterval = TimeSpan.FromSeconds(2); // flush interval
});features that go hard:
-
async batching โ
Channel<T>based, groups messages by interval (2s) or count (10), your app thread never blocks ๐ -
Polly resilience โ retry with exponential backoff, circuit breaker, per-request timeout via
Microsoft.Extensions.Http.Resilience๐ - best-effort โ if the webhook is bricked after retries, we drop the batch and keep vibing. never crashes your app no cap
-
min level filtering โ default
Warningso your chat dont get spammed with trace logs ๐ -
IHttpClientFactory โ proper socket management, named client
"LittyWebhooks"for custom config - Matrix hookshot format โ HTML-escaped output plus a text fallback, with exceptions in code blocks ๐ฃ๐ฅ
-
Slack Block Kit โ one plain-text header + one plain-text section per log, with
mrkdwndisabled so mentions, links, and formatting stay literal ๐ข๐๐ฅ - Slack limits handled โ headers stop at 150 Unicode scalars, sections at 3000, and batches at 49 logs + one header ๐๐ฅ
-
generic webhook config โ
AddLittyWebhookLogs()still lets advanced besties selectWebhookPlatform.MatrixorWebhookPlatform.Slackdirectly ๐ช๐ฅ
setting up a test channel? follow the Slack incoming webhook setup for the exact app settings, .env config, hermetic tests, and opt-in live smoke test ๐ข๐ฅ
all the boring framework messages you see every dotnet run:
| boring version ๐ | litty version ๐ฅ |
|---|---|
| Application started. Press Ctrl+C to shut down. | app is bussin and ready to slay bestie ๐ yeet Ctrl+C to dip out no cap |
| Now listening on: {url} | we vibing on {url} fr fr ๐ง |
| Content root path: {path} | content root living at {path} bestie ๐ |
| Hosting environment: {env} | we in our {env} era rn โจ |
| Application is shutting down... | app said aight imma head out ๐ |
| Request starting {details} | yo a request just slid in: {details} ๐ |
| Request finished {details} | request finished cooking: {details} ๐ณ |
plus hosting lifecycle, endpoint routing, and more fr fr
| level | emoji | vibe |
|---|---|---|
| Trace | ๐ | lowkey peeking |
| Debug | ๐ | investigating bestie |
| Information | ๐ฅ | bussin as usual |
| Warning | ๐ค | not it |
| Error | ๐ | big L |
| Critical | โ ๏ธ | its giving death |
builder.Logging.AddLittyLogs(options =>
{
options.RewriteMessages = true; // rewrite framework messages (default: true, thats the whole point)
options.UseColors = true; // ANSI colors (default: true)
options.ShortenCategories = true; // yeet namespace bloat (default: true)
options.UseUtcTimestamp = true; // UTC timestamps (default: true, international rizz)
options.TimestampFormat = "yyyy-MM-ddTHH:mm:ss.fffK"; // ISO 8601 with milliseconds (default)
options.TimestampFirst = false; // false = RFC 5424 (level first), true = observability style (timestamp first)
});your app logs are litty but dotnet build, dotnet test, dotnet publish, dotnet pack, and dotnet clean output is still giving corporate energy? install the tool and never look at boring terminal output again no cap
# install the tool
dotnet tool install --global LittyLogs.Tool
# litty-fy your test output (auto-shows ITestOutputHelper output too)
dotnet litty test
# litty-fy your build output
dotnet litty build
# litty-fy your publish output
dotnet litty publish
# litty-fy your pack output โ nupkgs go brrr ๐ฆ
dotnet litty pack
# litty-fy your clean output โ watch artifacts get yeeted in style ๐๏ธ
dotnet litty clean
# all args pass through to the underlying dotnet command
dotnet litty test --filter "FullyQualifiedName~MyTests"
dotnet litty build -c Release
dotnet litty publish -c Release --self-contained
dotnet litty pack -c Release
dotnet litty clean -c ReleasePassed! - Failed: 0, Passed: 66, Skipped: 0, Total: 66, Duration: 80 ms - LittyLogs.Tests.dll (net10.0)
[xUnit.net] scouting for tests in LittyLogs.Tests ๐
[xUnit.net] found the squad in LittyLogs.Tests ๐
[xUnit.net] lets gooo LittyLogs.Tests is cooking ๐ฅ
โ
LittyLogs.Tests.MyTest.SomeTest [26 ms]
[xUnit.net] LittyLogs.Tests absolutely ate no crumbs ๐
all tests ate and left no crumbs ๐
total vibes checked: 66
ate: 66 โ
cooked in 0.5 Seconds โฑ๏ธ
litty-logs only rewrites known framework messages. your custom log messages pass through with the bussin formatting (emojis, colors, short categories) but the actual message text stays exactly how you wrote it no cap
logger.LogInformation("my custom message stays exactly like this");
// output: [๐ฅ info] [2026-02-18T21:45:00.420Z] [MyService] my custom message stays exactly like thisseven example projects in examples/ so you can see litty-logs in every scenario:
| example | what it shows | run it |
|---|---|---|
WebApi |
startup demo with level-first โ timestamp-first โ JSON, then server runs | just example web |
HostedService |
startup demo with both timestamp modes, then background service vibes | just example hosted |
Console |
side-by-side text + JSON output comparison | just example console |
Xunit |
litty-fied xUnit test output with all log levels + TimestampFirst test | just example xunit |
Json |
structured JSON logging with both timestamp configs | just example json |
FileSink |
file sink with level-first โ timestamp-first โ JSON, reads em all back | just example filesink |
Webhooks |
dual webhook sink (Matrix + Slack) with mock listeners or live endpoints | just example webhooks |
every example auto-showcases ALL the modes when you run it โ no hidden flags, no secret handshakes. you run it, you see everything ๐
the webhooks example runs three demos: Matrix-only, Slack-only, and dual mode. set HOOKSHOT_URL and/or SLACK_WEBHOOK_URL in .env to go live โ any missing sink falls back to a local mock listener so the demo always eats. see the Slack local setup guide before using a real webhook ๐ช๐ฅ
this project uses just as the task runner. here are the vibes:
| recipe | what it does |
|---|---|
just build |
build the whole solution with litty-fied output ๐๏ธ๐ฅ |
just test |
run all tests with litty-fied output ๐งช๐ฅ |
just publish |
publish with litty-fied output ๐ค๐ฅ |
just pack |
pack all five NuGet packages with litty-fied output ๐ฆ๐ฅ |
just clean |
yeet all build artifacts with litty-fied output ๐๏ธ๐ฅ |
just release-next |
read-only git-cliff preview of the next strict SemVer; never installs anything ๐๐ฅ |
just release-notes |
read-only preview of the exact next changelog section ๐๐ฅ |
just example <name> |
run an example โ web, hosted, console, xunit, json, filesink, webhooks ๐ฅ |
just setup-completions |
install shell tab-completions for just example <tab>
|
tab-complete just example <tab> to see all available examples. works with zsh and bash:
# auto-install to your shell rc file
just setup-completions
# or source manually
source completions/just.zsh # zsh
source completions/just.bash # bashversion lives in one place: Directory.Build.props. all five packages inherit from it. main is the only long-lived branch; every feature, fix, chore, Renovate update, and rolling Release PR goes straight into it through a tiny squash PR ๐ณ๐ฅ
# start every change from fresh main
git switch main
git pull --ff-only origin main
git switch -c feature/my-bussin-change
# cook, run the just checks, push, and PR straight into main
just build --configuration Release
just test --configuration Release --no-build
just pack --no-build --output ./nupkgskeep branches tiny and auto-delete them after merge. main stays green and always represents the next shippable state; release math reads tags + squash commits, so deleted source branches are fully irrelevant no cap ๐ฅ
every merged releasable squash wakes release-pr.yml. the newest checksum-verified stable git-cliff 2.x reads commits since the latest immutable v* tag, updates the single release-pr branch, and opens or refreshes one chore(release): vX.Y.Z ๐ฅ PR. merge that PR and automation tags that exact main commit once; release.yml then builds, tests, packs, and ships it ๐ฅ
strict SemVer rules are intentionally simple:
-
feat(scope)!:or any conventional breaking marker โ major, even before 1.0 ๐ฅ๐ฅ -
feat:โ minor โจ๐ฅ -
fix:,perf:,revert:, andchore(deps):โ patch ๐๐ค๐ฅ - generic chores, docs, CI, tests, style, build, and refactors โ no release noise ๐๐ฅ
Forgejo manual dispatch supports auto, patch, minor, major, and promote. promote turns a prerelease like 1.0.0-rc.1 into 1.0.0 while copying its notes. manual dispatch only creates the PR; it never tags a side commit ๐๐ฅ
if a future release uses an RC, automatic bumps pause while that prerelease is current instead of looping into rc.2; explicit promote is the stable gate and includes any emergency conventional commits that landed during the freeze ๐๐ฅ
# urgent fixes are still tiny trunk PRs โ just move faster, not weirder
git switch main
git pull --ff-only origin main
git switch -c fix/production-is-cooked
# fix it, run the same checks, and PR into main
# after merge, the rolling Release PR updates automaticallythere is no special hotfix branch type and no merge-back. the fix lands on main, so the trunk and production history can never drift bestie ๐ณ๐ฅ
forgejo actions on a self-hosted runner handles the whole squad:
-
CI (
ci.yml) โ validates the conventional squash title, release policy, build, tests, and packages on every PR tomain๐ฅ -
Renovate (
renovate.json5) โ keeps NuGet packages and Forgejo Actions fresh onmain. non-major updates auto-merge after green CI; major updates stay regular PRs for human review no cap ๐ค๐ฅ -
Release PR (
release-pr.yml) โ calculates strict SemVer with the newest stable git-cliff2.x, maintains one rolling PR, then creates one immutable tag after merge ๐ง ๐๐ฅ -
Release (
release.yml) โ triggered byv*tags. the full pipeline hits THREE destinations:-
nuget.org โ all five
.nupkgfiles with--skip-duplicateso retries dont catch Ls -
forgejo releases โ via Gitea API with
.nupkgassets attached ๐ -
github mirror releases โ via
ghCLI with.nupkgassets on the mirror repo ๐
-
nuget.org โ all five
pipeline features that go hard:
- fully retryable โ rerunning the same tag is safe; an existing tag at the same commit is a no-op, while moving or deleting a shipped tag is never part of the flow ๐๐๐ฅ
-
pre-release auto-detection โ versions with
-(like0.1.0-dev,1.0.0-beta.1) auto-flag as pre-release on both platforms ๐งช -
changelog extraction โ release notes auto-pulled from
CHANGELOG.mdfor that professional rizz ๐ -
version sanity check โ tag must match
Directory.Build.propsor the pipeline tells you its not it ๐
see docs/runner-setup.md for runner setup and required secrets no cap
stuff that would go absolutely crazy but aint started yet. vibes only rn no cap
- ๐ฃ Matrix Client-Server API โ direct room messages for power users who want full HTML control instead of hookshot
- ๐จ custom webhook templates โ user-defined message format strings so you can make it look however you want
- ๐๏ธ zstd compression โ for file sink rotation (gzip is cool but zstd is faster and smaller fr fr)
- ๐ structured log enrichment โ auto-attach machine name, environment, correlation IDs to webhook messages
wanna see one of these happen? PRs are open bestie, or just vibe in the issues ๐
litty-logs takes security seriously even though we dont take ourselves seriously no cap. heres the tldr:
-
webhook URL validation โ SSRF prevention, only
http/httpsschemes allowed - log injection prevention โ newlines in messages get sanitized to spaces in text output
- content injection prevention โ Matrix gets escaped HTML; Slack gets plain-text Block Kit with markdown disabled, so tracking pixels, surprise mentions, and phishing links stay literal ๐๐ฅ
- HTTP category filtering โ prevents infinite recursion AND accidental webhook URL token exposure
full details in docs/security.md
found a vulnerability? dont yeet it in a public issue โ open a security advisory instead bestie ๐
MIT โ share the vibes bestie โ๏ธ