LogTailer.NET
1.3.1
dotnet add package LogTailer.NET --version 1.3.1
NuGet\Install-Package LogTailer.NET -Version 1.3.1
<PackageReference Include="LogTailer.NET" Version="1.3.1" />
<PackageVersion Include="LogTailer.NET" Version="1.3.1" />
<PackageReference Include="LogTailer.NET" />
paket add LogTailer.NET --version 1.3.1
#r "nuget: LogTailer.NET, 1.3.1"
#:package LogTailer.NET@1.3.1
#addin nuget:?package=LogTailer.NET&version=1.3.1
#tool nuget:?package=LogTailer.NET&version=1.3.1
LogTailer.NET
Stream tailed log files to the browser — a tail -F with a UI.
A dependency-free ASP.NET Core library with an embedded browser client and native WebSocket streaming, inspired by frontail. It ships two ways:
LogTailer.NET— a NuGet package you add to an existing ASP.NET Core app, mounting the log UI on a path of your choosing.LogTailerServer— a standalone web server you install on a box to read any service's log files, whether or not that service is .NET.
Features
- Follows one or more files like
tail -F, coping with rotation and truncation, cross-platform. - Seeds each new browser client with the last N lines, then streams live ones.
- Native WebSockets — no SignalR, no socket.io, no client-side build step.
- Word and line highlighting driven by your own code.
- Auto-scroll, pause, click-to-mark, regex filter box, and an unread-count tab badge.
- Zero package dependencies: just a framework reference to
Microsoft.AspNetCore.App.
Install
dotnet add package LogTailer.NET
Quick start
builder.Services.AddLogTailer(options =>
{
options.RequestPath = "/logs";
options.PageTitle = "My App";
options.Files.Add("logs/server.log");
});
var app = builder.Build();
app.UseLogTailer();
Or bind an appsettings.json section instead:
builder.Services.AddLogTailer(builder.Configuration.GetSection("LogTailer"));
{
"LogTailer": {
"RequestPath": "/logs",
"Files": [ "/var/log/myapp.log" ],
"StartingLines": 100
}
}
UseLogTailer() does nothing when no files are configured, so the call can stay in your pipeline
permanently and the feature switched on or off from config alone.
The endpoint is unauthenticated. Anyone who can reach RequestPath can read your logs, which
routinely contain tokens, personal data and internal hostnames. Guard the path with your own
middleware, or keep the app off the public internet.
The page is served no-store and its assets no-cache with an ETag. Leave that alone: the page is
rendered per request, so caching it freezes the title, the file list and the background colour at the
browser's first fetch instead of resolving them on every load.
Options
| Option | Description | Default |
|---|---|---|
RequestPath |
URL the UI is served from. / serves it at the application root |
/logs |
Files |
Paths to tail, absolute or relative | (empty — disabled) |
StartingLines |
History lines sent on connect, and the server ring-buffer size | 100 |
BrowserLines |
Maximum lines kept in the browser DOM | 2000 |
PageTitle |
Tab and topbar title. Used when no ILogTailerPolicy is registered, or when its GetPageTitleOverride() returns null |
Log Tailer |
IsTopbarHidden |
Hide the topbar | false |
IsIndentDisabled |
Disable the hanging indent on wrapped lines | false |
IsFileNamePrefixEnabled |
Prefix each line with its file name (automatic when tailing more than one file) | false |
BackgroundColor |
Page background | #060606 |
PollInterval |
How often the server re-reads a file when the file-system watcher misses a change | 500 ms |
IsPollingFallbackEnabled |
Serve lines over plain HTTP to browsers whose WebSocket cannot connect | true |
FallbackPollingInterval |
How often the browser asks for new lines once it has fallen back to polling | 2 s |
KeepAliveInterval |
How often each client is sent a WebSocket keepalive frame. Must stay below the shortest idle timeout between the server and the browser | 30 s |
PollInterval and FallbackPollingInterval are unrelated despite the similar names: the first is how
the server watches files, the second is how a browser without a working WebSocket asks for lines.
When the WebSocket won't connect
Some networks, proxies and browser configurations refuse WebSocket upgrades while ordinary HTTP keeps
working. Rather than show an empty page, the client falls back to polling RequestPath/poll after
three failed attempts, and keeps retrying the socket every 30 seconds in the background so it recovers
on its own.
While the fallback is active, two buttons appear in the topbar next to a status indicator:
| Button | Effect |
|---|---|
| Retry WS | Issues one ordinary HTTP request, shows its result, then retries the socket immediately. The request matters as much as the retry: it clears a pooled connection the browser wrongly believes is alive, and when that doesn't help, its result (basic 200, opaqueredirect, failed: …) names the failure. |
| Disable Polling | Stops the fallback so the raw WebSocket failure can be observed without polling masking it. Remembered for the browser session; press again to re-enable. |
Both transports carry the same messages and share a cursor, so switching between them neither loses
nor repeats a line. If a client is away long enough for lines to age out of the server's ring buffer,
the gap is marked inline as ==> N lines skipped <== rather than passed off as continuous.
Two query parameters help when diagnosing this:
| Parameter | Effect |
|---|---|
?nows=1 |
Skip the WebSocket entirely and go straight to polling. Useful for exercising the fallback without breaking a socket. |
?nopoll=1 |
Start with polling disabled, so a WebSocket failure shows as a dead page. |
Dynamic values — ILogTailerPolicy
Some things aren't known when the pipeline is built. A page title that carries the machine's public
IP, for instance, can't be a constant passed at startup — the IP isn't resolved yet. Register an
ILogTailerPolicy and LogTailer asks for those values while serving instead:
public sealed class MyLogTailerPolicy(IHostEnvironment environment) : ILogTailerPolicy
{
private static readonly HighlightConfig Highlight = new()
{
Words = new Dictionary<string, string>
{
["ERROR"] = "color: #ff4d4d;",
["WARN"] = "color: orange;",
},
Lines = new Dictionary<string, string>
{
["Unhandled exception"] = "font-weight: bold; color: #ff4d4d;",
},
};
public string GetPageTitleOverride() => $"MyApp [{environment.EnvironmentName}] {Environment.MachineName}";
public HighlightConfig? GetHighlight() => Highlight;
}
builder.Services.AddSingleton<ILogTailerPolicy, MyLogTailerPolicy>();
GetPageTitleOverride()is called on every page load, falling back toLogTailerOptions.PageTitlewhen it returns null or whitespace.GetHighlight()is called each time a browser connects, so changes reach new clients without a restart. Returningnulldisables highlighting.- The policy is optional, but it is the only way to configure highlighting — there's deliberately
no preset-file option in the library. See
LogTailerServerfor a host whose policy layers a file-driven preset on top of a built-in default. - Register exactly one. LogTailer throws at startup if it finds more than one registration, rather than silently picking one and leaving the other looking live.
- It must be safe to resolve as a singleton.
Highlighting
Words wraps every matching substring in a <span> carrying the given inline CSS. Lines styles
the whole line when it contains the key. Keys are literal text, matched case-sensitively — not
regular expressions — so failed and Failed are separate entries. Note the contrast with the UI's
filter box, which is case-insensitive.
Standalone server
LogTailerServer is a small ASP.NET Core host wrapping the library, for tailing services that
aren't .NET apps — nginx, PostgreSQL, a Python daemon, anything writing to a file.
dotnet publish LogTailerServer -c Release -o /opt/logtailer
How it reads its configuration depends on the environment, and the two paths share nothing:
- In Development it binds the
LogTailersection ofappsettings.json, so every option in the Options table is available from config or the command line. - In any other environment — which includes every published deployment — that section is ignored entirely and the options come from environment variables instead.
Development
ASPNETCORE_ENVIRONMENT=Development /opt/logtailer/LogTailerServer \
--LogTailer:Files:0=/var/log/nginx/error.log \
--LogTailer:PageTitle=nginx
Deployed
LOG_FILES=/var/log/nginx/error.log,/var/log/nginx/access.log \
APP_NAME=nginx \
STYLES_CONFIG=DEFAULT \
/opt/logtailer/LogTailerServer
| Variable | Description |
|---|---|
LOG_FILES |
Required. Comma-separated paths to tail. Startup fails if it is unset, or if any listed file does not exist |
APP_NAME |
Name in the page title, assembled as {APP_NAME} [{Environment}] {IP_ADDRESS}, defaulting to LogTailer |
IP_ADDRESS |
Appended to the page title when set — the host's public IP is the usual value |
STYLES_CONFIG |
DEFAULT for the built-in highlighting, or a path to a highlight JSON file. Any other value disables highlighting |
The rest is fixed on this path: the UI is served from the application root, with 100 seeded lines, a
2000-line browser buffer, a #060606 background and a 500 ms poll interval.
A STYLES_CONFIG file is read once at startup and takes the shape of a HighlightConfig. Property
names are case-insensitive; the highlight keys inside them are not:
{
"words": {
"ERROR": "color: #ff4d4d;",
"WARN": "color: orange;"
},
"lines": {
"FATAL": "font-weight: bold; color: #ff4d4d;"
}
}
The built-in DEFAULT set targets the level markers common to most log formats (ERROR, WARN,
INFO, DEBUG, TRACE, plus FATAL and stack traces). It lives in ServerLogTailerPolicy, which
is also where the STYLES_CONFIG handling sits — edit that class to change either.
It binds to http://127.0.0.1:5080 by default — loopback only, on the assumption that exposing
logs beyond the machine should be a deliberate act. Override with ASPNETCORE_URLS, and put a
reverse proxy or a VPN in front of it when you need remote access.
Using the UI
| Filter | Type in the box. Tab focuses it, Esc clears it, and the value round-trips through the ?filter= query parameter so a filtered view is shareable. Treated as a case-insensitive regex, falling back to substring matching when it doesn't compile. |
| Pause | Freezes the view and shows a running count of skipped lines. |
| Mark a line | Click it. |
| Unread count | While the window is unfocused, new lines increment a counter in the tab title. |
| Connection | A Disconnected - reconnecting... badge appears when the WebSocket drops, and clears once it is back. It sits outside the topbar, so it stays visible with IsTopbarHidden. |
Auto-scroll sticks to the bottom only when you're already there, so scrolling up to read isn't interrupted. ANSI escape sequences are stripped, and the client reconnects automatically if the server restarts.
A tailed file that stays quiet leaves the WebSocket silent, and proxies reap connections they think
are idle, so the server sends a keepalive frame every KeepAliveInterval. Behind a load balancer or
tunnel, check that value against the shortest idle timeout on the path -- an AWS ALB defaults to 60
seconds.
Requirements
.NET 10. The library targets net10.0 and framework-references Microsoft.AspNetCore.App.
License
MIT
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- No dependencies.
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|