Plan: Write CLI-style log files for Web→Core operations¶
Context¶
When a command or query is launched from Arius Web into Arius Core, Core's ILogger<T>
output is routed through the API host's shared ILoggerFactory, whose Serilog logger
(src/Arius.Api/Program.cs:11-14) has only a Console sink. So Web-launched operations
log to the API process stdout and nowhere else — never to the per-repository log
directory that the CLI writes to.
The CLI, by contrast, writes a per-invocation Serilog file to
~/.arius/{account}-{container}/logs/{timestamp}_{command}.txt
via CliBuilder.ConfigureAuditLogging(...) (src/Arius.Cli/CliBuilder.cs:152), using the
public path helper RepositoryLocalStatePaths.GetLogsDirectory(account, container)
(src/Arius.Core/Shared/RepositoryLocalStatePaths.cs:34).
Goal: Web→Core operations (both long-running jobs and queries) should also be logged
into that same logs/ directory the CLI uses, as a per-repository rolling log (daily,
with a 100 MB size cap), so a single log captures every operation for a repo without coupling
the log's lifetime to a provider's lifetime.
Approach¶
Maintain one shared Serilog logger per repository, cached in RepositoryProviderRegistry,
writing a rolling file into the repo's logs/ directory. Wire both read providers and job
providers to that logger. A single shared logger instance funnels all writes through one sink,
which is concurrency-safe (a per-provider file sink would not be) and decouples log lifetime
from the cached/disposed provider lifetime.
Read providers stay cached (they run a storage preflight + warm chunk-index/file-tree caches, so rebuilding per query would be wasteful); only the logger is shared, not the provider.
Changes¶
1. src/Arius.Api/Arius.Api.csproj — add the file-sink + formatter packages the CLI
already uses (Console + Extensions.Hosting are present):
- Serilog.Sinks.File
- Serilog.Expressions
- Serilog.Enrichers.Thread
2. src/Arius.Api/Composition/RepositoryProviderRegistry.cs — main change:
- Add a per-repo logger-factory cache, e.g.
Dictionary<long, ILoggerFactory> _repoLoggerFactories (guarded by the existing _gate),
or a ConcurrentDictionary.
- Add GetOrCreateRepoLoggerFactory(repositoryId, accountName, containerName) that builds once:
var logDir = RepositoryLocalStatePaths.GetLogsDirectory(accountName, containerName);
Directory.CreateDirectory(logDir);
var formatter = new ExpressionTemplate(
"[{@t:HH:mm:ss.fff}] [{@l:u3}] [T:{ThreadId}] [{Coalesce(Substring(SourceContext, LastIndexOf(SourceContext, '.') + 1), 'Arius')}] {@m}\n{@x}");
var serilog = new LoggerConfiguration()
.MinimumLevel.Information()
.Enrich.WithThreadId()
.WriteTo.Console()
.WriteTo.File(
formatter,
Path.Combine(logDir, "arius-.txt"),
rollingInterval: RollingInterval.Day,
fileSizeLimitBytes: 100L * 1024 * 1024,
rollOnFileSizeLimit: true,
retainedFileCountLimit: 31,
restrictedToMinimumLevel: LogEventLevel.Information)
.CreateLogger();
return LoggerFactory.Create(b => b.AddSerilog(serilog, dispose: true));
CliBuilder.ConfigureAuditLogging so CLI and Web logs
read identically; add a comment cross-referencing it. Optional follow-up: extract the format
string to a shared constant — Core must stay Serilog-free, so it can't live there.)
- In BuildAsync (after LoadConnection), replace the shared-factory wiring at
lines 94-96 with the per-repo factory:
var repoLoggerFactory = GetOrCreateRepoLoggerFactory(repositoryId, connection.AccountName, connection.Container);
services.AddSingleton(repoLoggerFactory);
services.AddSingleton(typeof(ILogger<>), typeof(Logger<>));
AddSingleton(instance) means the per-job
ServiceProvider's disposal does not dispose the shared logger (same pattern the code
already relies on for _loggerFactory), so a finished job's provider.DisposeAsync() /
Evict(repositoryId) leaves the rolling log open and intact.
- Dispose all cached per-repo factories in DisposeAsync (app shutdown) — this disposes each
SerilogLoggerProvider (dispose: true), flushing the file. Evict must not touch
_repoLoggerFactories.
The injected host _loggerFactory can remain for RepositoryProviderRegistry's own
_logger; only Core's per-repo provider logging is redirected.
Notes / non-goals¶
RepositoryLocalStatePaths.GetLogsDirectoryis alreadypublic— no Core change needed.- The CLI path (
CliBuilder) is unchanged. - Log level stays Information+ to match the CLI.
Verification¶
- Build:
dotnet build src/Arius.Api/Arius.Api.csproj(confirms the new package refs resolve). - Run a query end-to-end: start the API, open the Web app, browse a repository's snapshots
(
/api/repos/{id}/snapshots). Confirm a file appears at~/.arius/{account}-{container}/logs/arius-{yyyyMMdd}.txtcontaining Core log lines in the CLI format[HH:mm:ss.fff] [INF] [T:..] [Handler] message. - Run a job end-to-end: trigger an archive or restore from the Web UI. Confirm the same rolling file receives the archive/restore handler logs, and that a second job for the same repo appends to (does not truncate/lock) the same file.
- Cross-check with CLI: run the equivalent CLI command for the same account/container and
confirm both write into the same
logs/directory with matching line format. - Lifetime: confirm finishing a job (which disposes the job provider and calls
Evict) leaves the rolling log usable for subsequent queries; logs only flush/close fully on API shutdown.