Log Entries Feature Documentation¶
Query, stream, export, and manage persisted application logs through a stable application API.
Overview¶
The Log Entries feature provides an application-level API for querying, streaming, exporting, and cleaning up persisted logs. It does not replace the logging pipeline itself. Instead, it gives the rest of the devkit a stable contract for operational access to log data once logs have already been written to a store.
Application.Utilities defines the contract in ILogEntryService and the DTOs used by callers. Infrastructure projects provide concrete implementations, and Presentation.Web exposes a ready-made HTTP endpoint set.
What The Feature Covers¶
- paged log queries with continuation tokens
- live streaming of newly written log entries
- export to CSV, JSON, or plain text
- aggregated statistics by level and time interval
- cleanup and archival-oriented maintenance operations
- correlation-oriented filtering by trace, correlation, module, and log key metadata
Core Contract¶
The central abstraction is ILogEntryService.
It exposes these operations:
QueryAsync(...)StreamAsync(...)ExportAsync(...)GetStatisticsAsync(...)CleanupAsync(...)SubscribeAsync(...)
The application package also defines:
LogEntryQueryRequestLogEntryQueryResponseLogEntryModelLogEntryStatisticsModelLogEntryExportFormat
Setup¶
The log-entries feature needs more than just ILogEntryService. A working setup has four parts:
- the application must write structured logs into a persistent store
- the EF Core context used by the query service must implement
ILoggingContext - the host must register
ILogEntryServiceplus the maintenance queue and hosted service - the web host can optionally expose
LogEntryEndpoints
The DoFiesta example wires those pieces together in Program.cs and CoreDbContext.cs.
1. Persist Logs¶
The query feature only works if your logging pipeline writes log events into a durable store. Serilog needs to be configured with the MSSQL sink in appsettings.json.
"Serilog": {
"WriteTo": [
{
"Name": "MSSqlServer",
"Args": {
"connectionString": "Server=localhost,14333;Database=db;User Id=sa;Password=pw",
"sinkOptionsSection": {
"tableName": "__Logging_LogEntries",
"schemaName": "core",
"autoCreateSqlTable": false,
"batchPostingLimit": 1000,
"batchPeriod": "00:00:15"
},
"columnOptionsSection": {
"disableTriggers": true,
"clusteredColumnstoreIndex": false,
"primaryKeyColumnName": "Id",
"addStandardColumns": [
{
"ColumnName": "Id",
"DataType": "bigint"
},
"Message",
"MessageTemplate",
"Level",
"TimeStamp",
"Exception",
"LogEvent",
"TraceId",
"SpanId"
],
"removeStandardColumns": [ "Properties" ],
"timeStamp": {
"columnName": "TimeStamp",
"DataType": "datetimeoffset",
"convertToUtc": true
},
"additionalColumns": [
{
"ColumnName": "CorrelationId",
"PropertyName": "CorrelationId",
"DataType": "nvarchar",
"DataLength": 128,
"AllowNull": true
},
{
"ColumnName": "LogKey",
"PropertyName": "LogKey",
"DataType": "nvarchar",
"DataLength": 128,
"AllowNull": true
},
{
"ColumnName": "ModuleName",
"PropertyName": "ModuleName",
"DataType": "nvarchar",
"DataLength": 128,
"AllowNull": true
},
{
"ColumnName": "ThreadId",
"PropertyName": "ThreadId",
"DataType": "nvarchar",
"DataLength": 128,
"AllowNull": true
},
{
"ColumnName": "ShortTypeName",
"PropertyName": "ShortTypeName",
"DataType": "nvarchar",
"DataLength": 128,
"AllowNull": true
}
]
}
}
}
]
},
That sink writes into the core.__Logging_LogEntries table and includes the extra columns the devkit query model expects, such as:
CorrelationIdLogKeyModuleNameThreadIdShortTypeNameTraceIdSpanId
The host itself enables the configured logging pipeline through:
var builder = WebApplication.CreateBuilder(args);
builder.Host.ConfigureLogging();
builder.Host.ConfigureAppConfiguration();
2. Expose LogEntries In Your DbContext¶
Your EF Core context must implement ILoggingContext and expose a DbSet<LogEntry>.
using BridgingIT.DevKit.Infrastructure.EntityFramework;
using Microsoft.EntityFrameworkCore;
public class CoreDbContext(DbContextOptions<CoreDbContext> options) :
ModuleDbContextBase(options),
ILoggingContext
{
public DbSet<LogEntry> LogEntries { get; set; }
}
This is what allows LogEntryService<TContext> and LogEntryMaintenanceService<TContext> to query and maintain persisted log rows.
3. Register The Application And Maintenance Services¶
DoFiesta registers the query service, maintenance queue, and hosted maintenance worker directly in the web host:
using BridgingIT.DevKit.Application.Utilities;
using BridgingIT.DevKit.Infrastructure.EntityFramework;
using BridgingIT.DevKit.Presentation.Web;
builder.Services.AddScoped<ILogEntryService, LogEntryService<CoreDbContext>>();
builder.Services.AddSingleton<LogEntryMaintenanceQueue>();
if (!EnvironmentExtensions.IsBuildTimeOpenApiGeneration())
{
builder.Services.AddHostedService<LogEntryMaintenanceService<CoreDbContext>>();
}
builder.Services.AddEndpoints<LogEntryEndpoints>(builder.Environment.IsDevelopment());
What each registration does:
ILogEntryService: exposes the query, streaming, export, statistics, and cleanup APILogEntryMaintenanceQueue: collects cleanup/archive requestsLogEntryMaintenanceService<TContext>: processes queued maintenance work and periodic retention tasks in the backgroundLogEntryEndpoints: exposes the operational HTTP surface
4. Make Sure The Log Table Exists¶
Because the query service reads from persisted rows, your database schema must include the logging table used by the sink. The schema is expected to be managed explicitly instead of being created ad hoc by Serilog. That means it should have a migration in your infrastructure project that creates the logging table with the expected shape. The sink's autoCreateSqlTable option should be set to false to avoid conflicts.
In practice that means:
- your database must exist before you expect log queries to work
- the logging table shape must match the sink configuration
- the same database should be reachable by both the logging sink and
CoreDbContext
Minimal Host Example¶
var builder = WebApplication.CreateBuilder(args);
builder.Host.ConfigureLogging();
builder.Host.ConfigureAppConfiguration();
builder.Services.AddScoped<ILogEntryService, LogEntryService<AppDbContext>>();
builder.Services.AddSingleton<LogEntryMaintenanceQueue>();
builder.Services.AddHostedService<LogEntryMaintenanceService<AppDbContext>>();
builder.Services.AddEndpoints<LogEntryEndpoints>(builder.Environment.IsDevelopment());
And the corresponding DbContext contract:
public class AppDbContext(DbContextOptions<AppDbContext> options) :
DbContext(options),
ILoggingContext
{
public DbSet<LogEntry> LogEntries { get; set; }
}
Query Model¶
LogEntryQueryRequest supports operational filters instead of hard-coding one reporting view.
Important filters include:
StartTimeandEndTimeAgeLevelTraceIdCorrelationIdLogKeyModuleNameShortTypeNameSearchTextPageSizeContinuationToken
Important rules:
StartTimeandAgeare mutually exclusivePageSizemust be positiveSearchTextis validated to reject control characters
The service returns a LogEntryQueryResponse with:
ItemsContinuationTokenPageSize
That makes the API suitable for dashboards, admin APIs, and support tooling without forcing offset-based paging.
Typical Usage¶
Querying¶
public sealed class OperationsService(ILogEntryService logs)
{
public Task<LogEntryQueryResponse> GetRecentErrorsAsync(CancellationToken cancellationToken)
{
return logs.QueryAsync(new LogEntryQueryRequest
{
Age = TimeSpan.FromDays(1),
Level = LogLevel.Error,
PageSize = 200
}, cancellationToken);
}
}
Streaming¶
await foreach (var entry in logs.StreamAsync(
startTime: DateTimeOffset.UtcNow.AddMinutes(-5),
level: LogLevel.Warning,
pollingInterval: TimeSpan.FromSeconds(2),
cancellationToken: cancellationToken))
{
Console.WriteLine($"{entry.TimeStamp:u} {entry.Level} {entry.Message}");
}
Exporting¶
await using var stream = await logs.ExportAsync(
new LogEntryQueryRequest
{
Age = TimeSpan.FromDays(7),
ModuleName = "Sales"
},
LogEntryExportFormat.Csv,
cancellationToken);
Statistics¶
var stats = await logs.GetStatisticsAsync(
startTime: DateTimeOffset.UtcNow.AddDays(-1),
endTime: DateTimeOffset.UtcNow,
groupByInterval: TimeSpan.FromHours(1),
cancellationToken: cancellationToken);
HTTP Endpoints¶
Presentation.Web exposes this feature through LogEntryEndpoints.
By default the endpoint group is:
/api/_system/logentries
The built-in routes cover:
GET /api/_system/logentriesGET /api/_system/logentries/streamGET /api/_system/logentries/statsGET /api/_system/logentries/exportDELETE /api/_system/logentries
The default endpoint options require authorization, which makes these endpoints suitable for internal admin and support surfaces rather than public APIs.
Data Shape¶
Each LogEntryModel exposes operational metadata that is useful when diagnosing distributed flows:
- message and message template
- level and timestamp
- exception text
- trace and span identifiers
- correlation identifier
- log key
- module name
- thread id
- short type name
- structured log event properties
That makes the feature especially useful when combined with module scoping and distributed tracing.
Architecture¶
flowchart LR
App[Application code] --> Contract[ILogEntryService]
Contract --> Infra[Infrastructure implementation]
Infra --> Store[(Persisted log store)]
Web[Presentation.Web endpoints] --> Contract
The important boundary is that Application.Utilities owns the contract, not the persistence strategy. This lets the same query and export model work with different infrastructure implementations while keeping consumers stable.
Practical Notes¶
- Query paging is continuation-token based, not page-number based.
Ageis converted into a start time relative to the current moment.- Live streaming is polling-based and intended for operational dashboards and support tools.
- Cleanup is a maintenance operation, not a query concern.
- Export format is intentionally narrow and operational:
Csv,Json, orTxt.
Best Practices¶
- Use
ContinuationTokeninstead of trying to emulate offset paging. - Prefer
Agefor operational dashboards andStartTime/EndTimefor reporting screens. - Filter by
TraceIdorCorrelationIdwhen you need one end-to-end request or workflow. - Filter by
ModuleNameandLogKeywhen you need module-level operational slices. - Keep the HTTP endpoints behind authorization and treat them as operational tooling.
- Let infrastructure own retention and archival strategy; use
CleanupAsync(...)as the application-facing maintenance entry point.